From 2cb38709a56d50271c836e78821b28e6a03b68f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 13 Jun 2024 17:45:41 -0400 Subject: [PATCH 01/32] supervisor: new file This is a new entrypoint, mostly intended for Docker, so we have one simple process controlling the main Grist process. The purpose of this is to be able to make Grist easily restartable with a new environment. --- sandbox/supervisor.mjs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 sandbox/supervisor.mjs diff --git a/sandbox/supervisor.mjs b/sandbox/supervisor.mjs new file mode 100644 index 00000000..2508cb17 --- /dev/null +++ b/sandbox/supervisor.mjs @@ -0,0 +1,35 @@ +import {spawn} from 'child_process'; + +let grist; + +function startGrist(newConfig={}) { + saveNewConfig(newConfig); + // H/T https://stackoverflow.com/a/36995148/11352427 + grist = spawn('./sandbox/run.sh', { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'] + }); + grist.on('message', function(data) { + if (data.action === 'restart') { + console.log('Restarting Grist with new environment'); + + // Note that we only set this event handler here, after we have + // a new environment to reload with. Small chance of a race here + // in case something else sends a SIGINT before we do it + // ourselves further below. + grist.on('exit', () => { + grist = startGrist(data.newConfig); + }); + + grist.kill('SIGINT'); + } + }); + return grist; +} + +// Stub function +function saveNewConfig(newConfig) { + // TODO: something here to actually persist the new config before + // restarting Grist. +} + +startGrist(); From 20035fd58fa2874cd2bbf4a4ace4e88a33e9ec0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Fri, 7 Jun 2024 20:07:19 -0400 Subject: [PATCH 02/32] FlexServer: add new admin restart endpoint This adds an endpoint for the admin user to be able to signal to a controlling process to restart the server. This is intended for `docker-runner.mjs`. --- app/server/lib/FlexServer.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 46e4a508..5aed483d 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1883,6 +1883,22 @@ export class FlexServer implements GristServer { const probes = new BootProbes(this.app, this, '/api', adminMiddleware); probes.addEndpoints(); + this.app.post('/api/admin/restart', requireInstallAdmin, expressWrap(async (req, resp) => { + const newConfig = req.body.newConfig; + resp.on('finish', () => { + // If we have IPC with parent process (e.g. when running under + // Docker) tell the parent that we have a new environment so it + // can restart us. + if (process.send) { + process.send({ action: 'restart', newConfig }); + } + }); + // On the topic of http response codes, thus spake MDN: + // "409: This response is sent when a request conflicts with the current state of the server." + const status = process.send ? 200 : 409; + return resp.status(status).send(); + })); + // Restrict this endpoint to install admins this.app.get('/api/install/prefs', requireInstallAdmin, expressWrap(async (_req, resp) => { const activation = await this._activations.current(); From 1a64910be3c9a34641ae879437dd854a5df1fd9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 13 Jun 2024 17:45:47 -0400 Subject: [PATCH 03/32] Dockerfile: use docker-runner.mjs as new entrypoint --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f6cafa43..4af861cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,6 +113,7 @@ ADD bower_components /grist/bower_components ADD sandbox /grist/sandbox ADD plugins /grist/plugins ADD static /grist/static +ADD docker-runner.mjs /grist/docker-runner.mjs # Make optional pyodide sandbox available COPY --from=builder /grist/sandbox/pyodide /grist/sandbox/pyodide @@ -152,4 +153,4 @@ ENV \ EXPOSE 8484 ENTRYPOINT ["/usr/bin/tini", "-s", "--"] -CMD ["./sandbox/run.sh"] +CMD ["node", "./sandbox/supervisor.mjs"] From 91e0a62e9127c44bea394ee007b0def30e3e38f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 19 Jun 2024 16:42:48 -0400 Subject: [PATCH 04/32] Dockerfile: remove mention of docker-runner.mjs When rewriting 1a64910be3c9a34641ae879437dd854a5df1fd9b, I accidentally left a stray reference to docker-runner.mjs in there. Since this file doesn't exist anymore, this prevents Docker builds from happening. --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4af861cc..35148cdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,7 +113,6 @@ ADD bower_components /grist/bower_components ADD sandbox /grist/sandbox ADD plugins /grist/plugins ADD static /grist/static -ADD docker-runner.mjs /grist/docker-runner.mjs # Make optional pyodide sandbox available COPY --from=builder /grist/sandbox/pyodide /grist/sandbox/pyodide From 0ca120a4f43f9a8a9d501409d6d748dacba45b76 Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 20 Jun 2024 16:48:30 +0200 Subject: [PATCH 05/32] Add some database documentation (#937) Start documenting the databases including: * document ACL and other tables * Permissions * Groups, secrets, and other tables --------- Co-authored-by: jordigh --- documentation/database.md | 297 +++++++++ documentation/develop.md | 1 + .../BDD-doc-inheritance-after-change.svg | 4 + .../images/BDD-doc-inheritance-default.svg | 4 + documentation/images/BDD.drawio | 234 +++++++ documentation/images/homedb-schema.svg | 609 ++++++++++++++++++ .../images/ws-users-management-popup.png | Bin 0 -> 31918 bytes 7 files changed, 1149 insertions(+) create mode 100644 documentation/database.md create mode 100644 documentation/images/BDD-doc-inheritance-after-change.svg create mode 100644 documentation/images/BDD-doc-inheritance-default.svg create mode 100644 documentation/images/BDD.drawio create mode 100644 documentation/images/homedb-schema.svg create mode 100644 documentation/images/ws-users-management-popup.png diff --git a/documentation/database.md b/documentation/database.md new file mode 100644 index 00000000..fb8fecc6 --- /dev/null +++ b/documentation/database.md @@ -0,0 +1,297 @@ +# Database + +> [!WARNING] +> This documentation is meant to describe the state of the database. The reader should be aware that some undocumented changes may have been done after its last updates, and for this purpose should check the git history of this file. +> +> Also contributions are welcome! :heart: + +Grist manages two databases: +1. The Home Database; +2. The Document Database (also known as "the grist document"); + +The Home database is responsible for things related to the instance, such as: + - the users and the groups registered on the instance, + - the billing, + - the organisations (also called sites), the workspaces, + - the documents' metadata (such as ID, name, or workspace under which it is located); + - the access permissions (ACLs) to organisations, workspaces and documents (access to the content of the document is controlled by the document itself); + +A Grist Document contains data such as: + - The tables, pages, views data; + - The ACL *inside* to access to all or part of tables (rows or columns); + +## The Document Database + +### Inspecting the Document + +A Grist Document (with the `.grist` extension) is actually a SQLite database. You may download a document like [this one](https://api.getgrist.com/o/templates/api/docs/keLK5sVeyfPkxyaXqijz2x/download?template=false&nohistory=false) and inspect its content using a tool such as the `sqlite3` command: + +```` +$ sqlite3 Flashcards.grist +sqlite> .tables +Flashcards_Data _grist_TabBar +Flashcards_Data_summary_Card_Set _grist_TabItems +GristDocTour _grist_TableViews +_grist_ACLMemberships _grist_Tables +_grist_ACLPrincipals _grist_Tables_column +_grist_ACLResources _grist_Triggers +_grist_ACLRules _grist_Validations +_grist_Attachments _grist_Views +_grist_Cells _grist_Views_section +_grist_DocInfo _grist_Views_section_field +_grist_External_database _gristsys_Action +_grist_External_table _gristsys_ActionHistory +_grist_Filters _gristsys_ActionHistoryBranch +_grist_Imports _gristsys_Action_step +_grist_Pages _gristsys_FileInfo +_grist_REPL_Hist _gristsys_Files +_grist_Shares _gristsys_PluginData +```` + +:warning: If you want to ensure that you will not alter a document's contents, make a backup copy beforehand. + +### The migrations + +The migrations are handled in the Python sandbox in this code: +https://github.com/gristlabs/grist-core/blob/main/sandbox/grist/migrations.py + +For more information, please consult [the documentation for migrations](./migrations.md). + +## The Home Database + +The home database may either be a SQLite or a PostgreSQL database depending on how the Grist instance has been installed. For details, please refer to the `TYPEORM_*` env variables in the [README](https://github.com/gristlabs/grist-core/blob/main/README.md#database-variables). + +Unless otherwise configured, the home database is a SQLite file. In the default Docker image, it is stored at this location: `/persist/home.sqlite3`. + +The schema below is the same (except for minor differences in the column types), regardless of what the database type is. + +### The Schema + +The database schema is the following: + +![Schema of the home database](./images/homedb-schema.svg) + +> [!NOTE] +> For simplicity's sake, we have removed tables related to the billing and to the migrations. + +If you want to generate the above schema by yourself, you may run the following command using [SchemaCrawler](https://www.schemacrawler.com/) ([a docker image is available for a quick run](https://www.schemacrawler.com/docker-image.html)): +````bash +# You may adapt the --database argument to fit with the actual file name +# You may also remove the `--grep-tables` option and all that follows to get the full schema. +$ schemacrawler --server=sqlite --database=landing.db --info-level=standard \ + --portable-names --command=schema --output-format=svg \ + --output-file=/tmp/graph.svg \ + --grep-tables="products|billing_accounts|limits|billing_account_managers|activations|migrations" \ + --invert-match +```` + +### `orgs` table + +Stores organisations (also called "Team sites") information. + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| name | The name as displayed in the UI | +| domain | The part that should be added in the URL | +| owner | The id of the user who owns the org | +| host | ??? | + +### `workspaces` table + +Stores workspaces information. + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| name | The name as displayed in the UI | +| org_id | The organisation to which the workspace belongs | +| removed_at | If not null, stores the date when the workspaces has been placed in the trash (it will be hard deleted after 30 days) | + + +### `docs` table + +Stores document information that is not portable, which means that it does not store the document data nor the ACL rules (see the "Document Database" section). + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| name | The name as displayed in the UI | +| workspace_id | The workspace the document belongs to | +| is_pinned | Whether the document has been pinned or not | +| url_id | Short version of the `id`, as displayed in the URL | +| removed_at | If not null, stores the date when the workspaces has been placed in the trash (it will be hard deleted after 30 days) | +| options | Serialized options as described in the [DocumentOptions](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/UserAPI.ts#L125-L135) interface | +| grace_period_start | Specific to getgrist.com (TODO describe it) | +| usage | stats about the document (see [DocumentUsage](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/DocUsage.ts)) | +| trunk_id | If set, the current document is a fork (only from a tutorial), and this column references the original document | +| type | If set, the current document is a special one (as specified in [DocumentType](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/UserAPI.ts#L123)) | + +### `aliases` table + +Aliases for documents. + +FIXME: What's the difference between `docs.url_id` and `alias.url_id`? + +| Column name | Description | +| ------------- | -------------- | +| url_id | The URL alias for the doc_id | +| org_id | The organisation the document belongs to | +| doc_id | The document id | + +### `acl_rules` table + +Permissions to access either a document, workspace or an organisation. + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| permissions | The permissions granted to the group. See below. | +| type | Either equals to `ACLRuleOrg`, `ACLRuleWs` or `ACLRuleDoc` | +| org_id | The org id associated to this ACL (if set, workspace_id and doc_id are null) | +| workspace_id | The workspace id associated to this ACL (if set, doc_id and org_id are null) | +| doc_id | The document id associated to this ACL (if set, workspace_id and org_id are null) | +| group_id | The group of users for which the ACL applies | + + +The permissions are stored as an integer which is read in its binary form which allows to make bitwise operations: + +| Name | Value (binary) | Description | +| --------------- | --------------- | --------------- | +| VIEW | +0b00000001 | can view | +| UPDATE | +0b00000010 | can update | +| ADD | +0b00000100 | can add | +| REMOVE | +0b00001000 | can remove | +| SCHEMA_EDIT | +0b00010000 | can change schema of tables | +| ACL_EDIT | +0b00100000 | can edit the ACL (docs) or manage the teams (orgs and workspaces) of the resource | +| (reserved) | +0b01000000 | (reserved bit for the future) | +| PUBLIC | +0b10000000 | virtual bit meaning that the resource is shared publicly (not currently used) | + +You notice that the permissions can be then composed: + - EDITOR permissions = `VIEW | UPDATE | ADD | REMOVE` = `0b00000001+0b00000010+0b00000100+0b00001000` = `0b00001111` = `15` + - ADMIN permissions = `EDITOR | SCHEMA_EDIT` = `0b00001111+0b00010000` = `0b00011111` = `31` + - OWNER permissions = `ADMIN | ACL_EDIT` = `0b00011111+0b00100000` = `0b0011111` = `63` + +For more details about that part, please refer [to the code](https://github.com/gristlabs/grist-core/blob/192e2f36ba77ec67069c58035d35205978b9215e/app/gen-server/lib/Permissions.ts). + +### `secrets` table + +Stores secret informations related to documents, so the document may not store them (otherwise someone who downloads a doc may access them). Used to store the unsubscribe key and the target url of Webhooks. + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| value | The value of the secret (despite the table name, its stored unencrypted) | +| doc_id | The document id | + +### `prefs` table + +Stores special grants for documents for anyone having the key. + +| Column name | Description | +| ------------- | -------------- | +| id | The primary key | +| key | A long string secret to identify the share. Suitable for URLs. Unique across the database / installation. | +| link_id | A string to identify the share. This identifier is common to the home database and the document specified by docId. It need only be unique within that document, and is not a secret. | doc_id | The document to which the share belongs | +| options | Any overall qualifiers on the share | + +For more information, please refer [to the comments in the code](https://github.com/gristlabs/grist-core/blob/192e2f36ba77ec67069c58035d35205978b9215e/app/gen-server/entity/Share.ts). + +### `groups` table + +The groups are entities that may contain either other groups and/or users. + +| Column name | Description | +|--------------- | --------------- | +| id | The primary key | +| name | The name (see the 5 types of groups below) | + +Only 5 types of groups exist, which corresponds actually to Roles (for the permissions, please refer to the [ACL rules permissions details](#acl-permissions)): + - `owners` (see the `OWNERS` permissions) + - `editors` (see the `EDITORS` permissions) + - `viewers` (see the `VIEWS` permissions) + - `members` + - `guests` + +`viewers`, `members` and `guests` have basically the same rights (like viewers), the only difference between them is that: + - `viewers` are explicitly allowed to view the resource and its descendants; + - `members` are specific to the organisations and are meant to allow access to be granted to individual documents or workspaces, rather than the full team site. + - `guests` are (FIXME: help please on this one :)) + +Each time a resource is created, the groups corresponding to the roles above are created (except the `members` which are specific to organisations). + +### `group_groups` table + +The table which allows groups to contain other groups. It is also used for the inheritance mechanism (see below). + +| Column name | Description | +|--------------- | --------------- | +| group_id | The id of the group containing the subgroup | +| subgroup_id | The id of the subgroup | + +### `group_users` table + +The table which assigns users to groups. + +| Column name | Description | +|--------------- | --------------- | +| group_id | The id of the group containing the user | +| user_id | The id of the user | + +### `groups`, `group_groups`, `group_users` and inheritances + +We mentioned earlier that the groups currently holds the roles with the associated permissions. + +The database stores the inheritances of rights as described below. + +Let's imagine that a user is granted the role of *Owner* for the "Org1" organisation, s/he therefore belongs to the group "Org1 Owners" (whose ID is `id_org1_owner_grp`) which also belongs to the "WS1 Owners" (whose ID is `id_ws1_owner_grp`) by default. In other words, this user is by default owner of both the Org1 organization and of the WS1 workspace. + +The below schema illustrates both the inheritance of between the groups and the state of the database: + +![BDD state by default](./images/BDD-doc-inheritance-default.svg) + +This inheritance can be changed through the Users management popup in the Contextual Menu for the Workspaces: + +![The drop-down list after "Inherit access:" in the workspaces Users Management popup](./images/ws-users-management-popup.png) + +If you change the inherit access to "View Only", here is what happens: + +![BDD state after inherit access has changed, the `group_groups.group_id` value has changed](./images/BDD-doc-inheritance-after-change.svg) + +The Org1 owners now belongs to the "WS1 Viewers" group, and the user despite being *Owner* of "Org1" can only view the workspace WS1 and its documents because s/he only gets the Viewer role for this workspace. Regarding the database, `group_groups` which holds the group inheritance has been updated, so the parent group for `id_org1_owner_grp` is now `id_ws1_viewers_grp`. + +### `users` table + +Stores `users` information. + +| Column name | Description | +|--------------- | --------------- | +| id | The primary key | +| name | The user's name | +| api_key | If generated, the [HTTP API Key](https://support.getgrist.com/rest-api/) used to authenticate the user | +| picture | The URL to the user's picture (must be provided by the SSO Identity Provider) | +| first_login_at | The date of the first login | +| is_first_time_user | Whether the user discovers Grist (used to trigger the Welcome Tour) | +| options | Serialized options as described in [UserOptions](https://github.com/gristlabs/grist-core/blob/513e13e6ab57c918c0e396b1d56686e45644ee1a/app/common/UserAPI.ts#L169-L179) interface | +| connect_id | Used by [GristConnect](https://github.com/gristlabs/grist-ee/blob/5ae19a7dfb436c8a3d67470b993076e51cf83f21/ext/app/server/lib/GristConnect.ts) in Enterprise Edition to identify user in external provider | +| ref | Used to identify a user in the automated tests | + +### `logins` table + +Stores information related to the identification. + +> [!NOTE] +> A user may have many `logins` records associated to him/her, like several emails used for identification. + + +| Column name | Description | +|--------------- | --------------- | +| id | The primary key | +| user_id | The user's id | +| email | The normalized email address used for equality and indexing (specifically converted to lower case) | +| display_email | The user's email address as displayed in the UI | + +### The migrations + +The database migrations are handled by TypeORM ([documentation](https://typeorm.io/migrations)). The migration files are located at `app/gen-server/migration` and are run at startup (so you don't have to worry about running them yourself). + diff --git a/documentation/develop.md b/documentation/develop.md index e95b5b01..4595b60c 100644 --- a/documentation/develop.md +++ b/documentation/develop.md @@ -130,6 +130,7 @@ Check out this repository: https://github.com/gristlabs/grist-widget#readme Some documentation to help you starting developing: - [Overview of Grist Components](./overview.md) + - [The database](./database.md) - [GrainJS & Grist Front-End Libraries](./grainjs.md) - [GrainJS Documentation](https://github.com/gristlabs/grainjs/) (The library used to build the DOM) - [The user support documentation](https://support.getgrist.com/) diff --git a/documentation/images/BDD-doc-inheritance-after-change.svg b/documentation/images/BDD-doc-inheritance-after-change.svg new file mode 100644 index 00000000..0899246a --- /dev/null +++ b/documentation/images/BDD-doc-inheritance-after-change.svg @@ -0,0 +1,4 @@ + + + +
Org1
Workspace1
Some user
group_users
group_id
user_id
id_org1_owner_grp
id_some_user
Org1 Owners
Ws1 Owners
NEW
Ws1 Viewers

group_groups
group_id
subgroup_id
id_ws1_owner_grp
id_ws1_viewers_grp
id_org1_owner_grp
\ No newline at end of file diff --git a/documentation/images/BDD-doc-inheritance-default.svg b/documentation/images/BDD-doc-inheritance-default.svg new file mode 100644 index 00000000..1fc21857 --- /dev/null +++ b/documentation/images/BDD-doc-inheritance-default.svg @@ -0,0 +1,4 @@ + + + +
Org1
Org1
Workspace1
Workspace1
Some user
Some...
group_users
group_id
group_id
user_id
user_id
id_org1_owner_grp
id_org1_owner_grp
id_some_user
id_some_user
group_groups
group_id
group_id
subgroup_id
subgroup_id
id_ws1_owner_grp
id_ws1_owner_grp
id_org1_owner_grp
id_org1_owner_grp
Org1 Owners
Org1 Own...
Ws1 Owners
Ws1 Owne...
\ No newline at end of file diff --git a/documentation/images/BDD.drawio b/documentation/images/BDD.drawio new file mode 100644 index 00000000..7e7fe9aa --- /dev/null +++ b/documentation/images/BDD.drawio @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/images/homedb-schema.svg b/documentation/images/homedb-schema.svg new file mode 100644 index 00000000..eed9fc31 --- /dev/null +++ b/documentation/images/homedb-schema.svg @@ -0,0 +1,609 @@ + + + + + + +SchemaCrawler_Diagram + +generated by +SchemaCrawler 16.21.2 +generated on +2024-04-15 14:21:22 + + + +acl_rules_53bd8961 + +acl_rules + +[table] +id + +INTEGER NOT NULL + +auto-incremented +permissions + +INTEGER NOT NULL +type + +VARCHAR NOT NULL +workspace_id + +INTEGER +org_id + +INTEGER +doc_id + +VARCHAR +group_id + +INTEGER + + + + +docs_2f969a + +docs + +[table] +id + +VARCHAR NOT NULL +name + +VARCHAR NOT NULL +created_at + +DATETIME NOT NULL +updated_at + +DATETIME NOT NULL +workspace_id + +INTEGER +is_pinned + +BOOLEAN NOT NULL +url_id + +VARCHAR +removed_at + +DATETIME +options + +VARCHAR +grace_period_start + +DATETIME +usage + +VARCHAR +created_by + +INTEGER +trunk_id + +TEXT +type + +TEXT + + + + +acl_rules_53bd8961:w->docs_2f969a:e + + + + + + + + + + +groups_b63e4e33 + +groups + +[table] +id + +INTEGER NOT NULL + +auto-incremented +name + +VARCHAR NOT NULL + + + + +acl_rules_53bd8961:w->groups_b63e4e33:e + + + + + + + + + + +orgs_34a26e + +orgs + +[table] +id + +INTEGER NOT NULL + +auto-incremented +name + +VARCHAR NOT NULL +domain + +VARCHAR +created_at + +DATETIME NOT NULL +updated_at + +DATETIME NOT NULL +owner_id + +INTEGER +billing_account_id + +INTEGER +host + +VARCHAR + + + + +acl_rules_53bd8961:w->orgs_34a26e:e + + + + + + + + + + +workspaces_e61add + +workspaces + +[table] +id + +INTEGER NOT NULL + +auto-incremented +name + +VARCHAR NOT NULL +created_at + +DATETIME NOT NULL +updated_at + +DATETIME NOT NULL +org_id + +INTEGER +removed_at + +DATETIME + + + + +acl_rules_53bd8961:w->workspaces_e61add:e + + + + + + + + + + +aliases_c97dc35d + +aliases + +[table] +url_id + +VARCHAR NOT NULL +org_id + +INTEGER NOT NULL +doc_id + +VARCHAR +created_at + +DATETIME NOT NULL + + + + +aliases_c97dc35d:w->docs_2f969a:e + + + + + + + + + + +aliases_c97dc35d:w->orgs_34a26e:e + + + + + + + + + + +docs_2f969a:w->docs_2f969a:e + + + + + + + + + + +docs_2f969a:w->workspaces_e61add:e + + + + + + + + + + +users_6a70267 + +users + +[table] +id + +INTEGER NOT NULL + +auto-incremented +name + +VARCHAR NOT NULL +api_key + +VARCHAR +picture + +VARCHAR +first_login_at + +DATETIME +is_first_time_user + +INTEGER NOT NULL +options + +VARCHAR +connect_id + +VARCHAR +"ref" + +VARCHAR NOT NULL + + + + +docs_2f969a:w->users_6a70267:e + + + + + + + + + + +secrets_756efc22 + +secrets + +[table] +id + +VARCHAR NOT NULL +"value" + +VARCHAR NOT NULL +doc_id + +VARCHAR NOT NULL + + + + +secrets_756efc22:w->docs_2f969a:e + + + + + + + + + + +shares_ca2520d3 + +shares + +[table] +id + +INTEGER NOT NULL + +auto-incremented +key + +VARCHAR NOT NULL +doc_id + +VARCHAR NOT NULL +link_id + +VARCHAR NOT NULL +options + +VARCHAR NOT NULL + + + + +shares_ca2520d3:w->docs_2f969a:e + + + + + + + + + + +group_groups_dfa1d7f3 + +group_groups + +[table] +group_id + +INTEGER NOT NULL +subgroup_id + +INTEGER NOT NULL + + + + +group_groups_dfa1d7f3:w->groups_b63e4e33:e + + + + + + + + + + +group_groups_dfa1d7f3:w->groups_b63e4e33:e + + + + + + + + + + +group_users_41cb40a7 + +group_users + +[table] +group_id + +INTEGER NOT NULL +user_id + +INTEGER NOT NULL + + + + +group_users_41cb40a7:w->groups_b63e4e33:e + + + + + + + + + + +group_users_41cb40a7:w->users_6a70267:e + + + + + + + + + + +logins_be987289 + +logins + +[table] +id + +INTEGER NOT NULL + +auto-incremented +user_id + +INTEGER NOT NULL +email + +VARCHAR NOT NULL +display_email + +VARCHAR NOT NULL + + + + +logins_be987289:w->users_6a70267:e + + + + + + + + + + +id_dc7c64b2 +billing_accounts.id + + + +orgs_34a26e:w->id_dc7c64b2:e + + + + + + + + + + +orgs_34a26e:w->users_6a70267:e + + + + + + + + + + + +prefs_660170f + +prefs + +[table] +org_id + +INTEGER +user_id + +INTEGER +prefs + +VARCHAR NOT NULL + + + + +prefs_660170f:w->orgs_34a26e:e + + + + + + + + + + +prefs_660170f:w->users_6a70267:e + + + + + + + + + + +workspaces_e61add:w->orgs_34a26e:e + + + + + + + + + + +user_id_2d5fdf94 +billing_account_managers.user_id + + + +user_id_2d5fdf94:w->users_6a70267:e + + + + + + + + + + diff --git a/documentation/images/ws-users-management-popup.png b/documentation/images/ws-users-management-popup.png new file mode 100644 index 0000000000000000000000000000000000000000..9b18aaa43bba1103fbcc549df1d929952c74d6d3 GIT binary patch literal 31918 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 literal 0 HcmV?d00001 From 7cbb9decc00789f20360b67cfaf71f2a25c1b1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 20 Jun 2024 18:24:45 -0400 Subject: [PATCH 06/32] README: Rewrite boot page section to reflect new admin page We removed the boot page in 5dc4706dc7ff3e3fb1adb0ab81e4d8559a021b7c, but we forgot to update the README to reflect this. --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fad5aa62..ee9099d9 100644 --- a/README.md +++ b/README.md @@ -117,22 +117,24 @@ You can find a lot more about configuring Grist, setting up authentication, and running it on a public server in our [Self-Managed Grist](https://support.getgrist.com/self-managed/) handbook. -## Activating the boot page for diagnosing problems +## The administrator panel -You can turn on a special "boot page" to inspect the status of your -installation. Just visit `/boot` on your Grist server for instructions. -Since it is useful for the boot page to be available even when authentication -isn't set up, you can give it a special access key by setting `GRIST_BOOT_KEY`. +You can turn on a special admininistrator panel to inspect the status +of your installation. Just visit `/admin` on your Grist server for +instructions. Since it is useful for the admin panel to be +available even when authentication isn't set up, you can give it a +special access key by setting `GRIST_BOOT_KEY`. ``` docker run -p 8484:8484 -e GRIST_BOOT_KEY=secret -it gristlabs/grist ``` -The boot page should then be available at `/boot/`. We are -starting to collect probes for common problems there. If you hit a problem that -isn't covered, it would be great if you could add a probe for it in +The boot page should then be available at +`/admin?boot-key=`. We are collecting probes for +common problems there. If you hit a problem that isn't covered, it +would be great if you could add a probe for it in [BootProbes](https://github.com/gristlabs/grist-core/blob/main/app/server/lib/BootProbes.ts). -Or file an issue so someone else can add it, we're just getting start with this. +You may instead file an issue so someone else can add it. ## Building from source From 6c2079166c5931c75e4e0456e4dbc79f729e694a Mon Sep 17 00:00:00 2001 From: Roman Holinec <3ko@pixeon.sk> Date: Sun, 23 Jun 2024 05:05:48 +0000 Subject: [PATCH 07/32] Translated using Weblate (Slovak) Currently translated at 27.5% (368 of 1334 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sk/ --- static/locales/sk.client.json | 220 +++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 4 deletions(-) diff --git a/static/locales/sk.client.json b/static/locales/sk.client.json index 0ee86e76..ea46a243 100644 --- a/static/locales/sk.client.json +++ b/static/locales/sk.client.json @@ -41,7 +41,8 @@ "Allow everyone to copy the entire document, or view it in full in fiddle mode.\nUseful for examples and templates, but not for sensitive data.": "Umožnite každému skopírovať celý dokument alebo ho zobraziť celý vo fiddle móde.\n Užitočné pre príklady a šablóny, ale nie pre citlivé údaje.", "Saved": "Uložené", "Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Umožniť editorom upravovať štruktúru (napr. upravovať a mazať tabuľky, stĺpce, rozloženia) a písať vzorce, ktoré umožňujú prístup ku všetkým údajom bez ohľadu na obmedzenia čítania.", - "Remove {{- name }} user attribute": "Odstrániť používateľský atribút {{- name }}" + "Remove {{- name }} user attribute": "Odstrániť používateľský atribút {{- name }}", + "Add Table-wide Rule": "Pridať Pravidlo pre celú tabuľku" }, "AccountPage": { "API": "API", @@ -150,7 +151,9 @@ "Create separate series for each value of the selected column.": "Vytvoriť samostatné série pre každú hodnotu vybratého stĺpca.", "Pick a column": "Vybrať stĺpec", "Toggle chart aggregation": "Prepnúť združovanie grafu", - "selected new group data columns": "vybrať nové stĺpce skupiny dát" + "selected new group data columns": "vybrať nové stĺpce skupiny dát", + "Each Y series is followed by a series for the length of error bars.": "Po každej sérii Y nasleduje séria dlhých chybových pruhov.", + "Each Y series is followed by two series, for top and bottom error bars.": "Po každej sérii Y nasledujú dve série pre horný a dolný chybový pruh." }, "ColumnFilterMenu": { "All Shown": "Všetko zobrazené", @@ -187,7 +190,9 @@ "Widget needs {{fullAccess}} to this document.": "Widget vyžaduje {{fullAccess}} k tomuto dokumentu.", "No document access": "Bez prístupu k dokumentu", "Clear selection": "Vyčistiť výber", - "No {{columnType}} columns in table.": "V tabuľke nie sú žiadne stĺpce {{columnType}}." + "No {{columnType}} columns in table.": "V tabuľke nie sú žiadne stĺpce {{columnType}}.", + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "stĺpce {{wrongTypeCount}} iné ako {{columnType}} sa nezobrazujú", + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "stĺpec {{wrongTypeCount}} iný ako {{columnType}} sa nezobrazuje" }, "AppModel": { "This team site is suspended. Documents can be read, but not modified.": "Táto tímová stránka je pozastavená. Dokumenty je možné čítať, ale nie upravovať." @@ -219,6 +224,213 @@ "Activity": "Aktivita", "Beta": "Beta", "Compare to Previous": "Porovnať s Predchádzajúcim", - "Compare to Current": "Porovnať s Aktuálnym" + "Compare to Current": "Porovnať s Aktuálnym", + "Open Snapshot": "Otvoriť Snímok", + "Snapshots": "Snímok", + "Snapshots are unavailable.": "Snímky nie sú k dispozícii.", + "Only owners have access to snapshots for documents with access rules.": "Prístup k snímkom dokumentov s pravidlami prístupu majú iba vlastníci." + }, + "DocMenu": { + "(The organization needs a paid plan)": "(Organizácia potrebuje platený plán)", + "Access Details": "Podrobnosti Prístupu", + "By Date Modified": "Podľa Dátumu Zmeny", + "Document will be moved to Trash.": "Dokument bude presunutý do Koša.", + "Edited {{at}}": "Upravené {{at}}", + "Examples and Templates": "Príklady a Šablóny", + "Examples & Templates": "Príklady & Šablóny", + "Manage Users": "Spravovať Používateľov", + "More Examples and Templates": "Ďalšie Príklady a Šablóny", + "Move": "Presunúť", + "Other Sites": "Iné Stránky", + "Permanently Delete \"{{name}}\"?": "Natrvalo Odstrániť „{{name}}“?", + "Pin Document": "Pripnúť Dokument", + "Pinned Documents": "Pripnuté Dokumenty", + "Remove": "Odstrániť", + "Rename": "Premenovať", + "Requires edit permissions": "Vyžaduje povolenia na úpravy", + "To restore this document, restore the workspace first.": "Ak chcete tento dokument obnoviť, najskôr obnovte pracovný priestor.", + "Trash": "Kôš", + "Trash is empty.": "Kôš je prázdny.", + "Unpin Document": "Odopnúť Dokument", + "Workspace not found": "Pracovný priestor sa nenašiel", + "You are on your personal site. You also have access to the following sites:": "Nachádzate sa na svojej osobnej stránke. Máte tiež prístup k nasledujúcim stránkam:", + "All Documents": "Všetky dokumenty", + "Current workspace": "Aktuálny pracovný priestor", + "Deleted {{at}}": "Odstránené {{at}}", + "By Name": "Podľa Názvu", + "Delete": "Odstrániť", + "Delete Forever": "Odstrániť Navždy", + "Discover More Templates": "Objaviť Ďalšie Šablóny", + "Document will be permanently deleted.": "Dokument bude natrvalo odstránený.", + "Documents stay in Trash for 30 days, after which they get deleted permanently.": "Dokumenty zostanú v koši 30 dní, potom sa natrvalo odstránia.", + "Featured": "Odporúčané", + "Move {{name}} to workspace": "Presunúť {{name}} do pracovného priestoru", + "This service is not available right now": "Táto služba nie je momentálne dostupná", + "Restore": "Obnoviť", + "You are on the {{siteName}} site. You also have access to the following sites:": "Nachádzate sa na stránke {{siteName}}. Máte tiež prístup k nasledujúcim stránkam:", + "You may delete a workspace forever once it has no documents in it.": "Keď pracovný priestor neobsahuje žiadne dokumenty, môžete ho natrvalo odstrániť.", + "Delete {{name}}": "Odstrániť {{name}}" + }, + "DocPageModel": { + "Enter recovery mode": "Spustiť režim obnovenia", + "Error accessing document": "Chyba pri prístupe k dokumentu", + "Reload": "Znovu načítať", + "Add Empty Table": "Pridať Prázdnu Tabuľku", + "Add Page": "Pridať Stránku", + "Add Widget to Page": "Pridať Miniaplikáciu na Stránku", + "Document owners can attempt to recover the document. [{{error}}]": "Vlastníci dokumentu sa môžu pokúsiť dokument obnoviť. [{{chyba}}]", + "Sorry, access to this document has been denied. [{{error}}]": "Ľutujeme, prístup k tomuto dokumentu bol odmietnutý. [{{error}}]", + "You can try reloading the document, or using recovery mode. Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. It also disables formulas. [{{error}}]": "Môžete skúsiť znova načítať dokument alebo použiť režim obnovenia. Režim obnovenia otvorí dokument tak, aby bol plne prístupný pre vlastníkov a neprístupný pre ostatných. Zakáže tiež vzorce. [{{error}}]", + "You do not have edit access to this document": "Nemáte prístup k úpravám tohto dokumentu" + }, + "DocTour": { + "Cannot construct a document tour from the data in this document. Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location.": "Nie je možné vytvoriť prehliadku dokumentu z údajov v tomto dokumente. Uistite sa, že existuje tabuľka s názvom GristDocTour so stĺpcami Title, Body, Placement a Location.", + "No valid document tour": "Neplatná prehliadka dokumentov" + }, + "DocumentSettings": { + "Currency:": "Mena:", + "Document Settings": "Nastavenia Dokumentu", + "Save and Reload": "Uložiť a znova Načítať", + "This document's ID (for API use):": "ID tohto dokumentu (na použitie API):", + "Time Zone:": "Časové Pásmo:", + "Manage Webhooks": "Spravovať Webhooks", + "Webhooks": "Webhooks", + "API Console": "API Konzola", + "API URL copied to clipboard": "Adresa URL rozhrania API bola skopírovaná do schránky", + "API console": "API konzola", + "API documentation.": "API dokumentácia.", + "Base doc URL: {{docApiUrl}}": "Základná URL dokumentu: {{docApiUrl}}", + "Coming soon": "Už čoskoro", + "Copy to clipboard": "Skopírovať do schránky", + "Currency": "Mena", + "Data Engine": "Dátový Stroj", + "Default for DateTime columns": "Predvoľba pre stĺpce DateTime", + "Document ID": "ID Dokumentu", + "Find slow formulas": "Vyhľadať pomalé vzorce", + "For number and date formats": "Pre čísla a formáty dátumu", + "Formula times": "Vzorec časov", + "Hard reset of data engine": "Tvrdý reset dátového stroja", + "ID for API use": "ID pre použitie API", + "Locale": "Miestne", + "Manage webhooks": "Spravovať webhooks", + "Python version used": "Použitá verzia Pythonu", + "Reload": "Znovu načítať", + "Time Zone": "Časové Pásmo", + "Try API calls from the browser": "Skúsiť volania API z prehliadača", + "python2 (legacy)": "python2 (zastaralé)", + "python3 (recommended)": "python3 (odporúčané)", + "Cancel": "Zrušiť", + "Force reload the document while timing formulas, and show the result.": "Vynútiť opätovné načítanie dokumentu pri časovaní vzorcov a zobraziť výsledok.", + "Formula timer": "Časovač Vzorca", + "Reload data engine": "Znovu načítať dátový stroj", + "Reload data engine?": "Znovu načítať dátový stroj?", + "Start timing": "Spustiť časovanie", + "Stop timing...": "Zastaviť časovač...", + "Time reload": "Znova načítať čas", + "Timing is on": "Časovanie je zapnuté", + "You can make changes to the document, then stop timing to see the results.": "Môžete vykonať zmeny v dokumente a potom zastaviť časovanie, aby ste videli výsledky.", + "Local currency ({{currency}})": "Miestna mena ({{currency}})", + "Save": "Uložiť", + "Engine (experimental {{span}} change at own risk):": "Motor (experimentálna {{span}} zmena na vlastné riziko):", + "Locale:": "Miestne:", + "Ok": "OK", + "API": "API", + "Document ID copied to clipboard": "ID dokumentu bolo skopírované do schránky", + "Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}": "ID dokumentu, ktoré sa má použiť vždy, keď REST API požaduje {{docId}}. Pozrieť {{apiURL}}", + "For currency columns": "Pre stĺpce meny", + "Python": "Python", + "Notify other services on doc changes": "Upozorniť ostatné služby na zmeny dokumentu" + }, + "DocumentUsage": { + "Attachments Size": "Veľkosť Príloh", + "Contact the site owner to upgrade the plan to raise limits.": "Kontaktujte vlastníka lokality, aby inovoval plán a zvýšil limity.", + "For higher limits, ": "Pre vyššie limity, ", + "Rows": "Riadky", + "Usage": "Použitie", + "Usage statistics are only available to users with full access to the document data.": "Štatistiky používania sú dostupné len pre používateľov s úplným prístupom k údajom dokumentu.", + "start your 30-day free trial of the Pro plan.": "začnite svoju 30-dňovú bezplatnú skúšobnú verziu plánu Pro.", + "Data Size": "Veľkosť Údajov" + }, + "DuplicateTable": { + "Copy all data in addition to the table structure.": "Skopírujte všetky údaje okrem štruktúry tabuľky.", + "Instead of duplicating tables, it's usually better to segment data using linked views. {{link}}": "Namiesto duplikovania tabuliek je zvyčajne lepšie segmentovať údaje pomocou prepojených zobrazení. {{link}}", + "Name for new table": "Názov novej tabuľky", + "Only the document default access rules will apply to the copy.": "Na kópiu sa budú vzťahovať iba predvolené pravidlá prístupu k dokumentu." + }, + "ExampleInfo": { + "Afterschool Program": "Mimoškolský Program", + "Check out our related tutorial for how to link data, and create high-productivity layouts.": "Pozrite si náš súvisiaci návod, ako prepojiť údaje a vytvoriť vysoko produktívne rozloženia.", + "Investment Research": "Investičný Výskum", + "Lightweight CRM": "Ľahké CRM", + "Tutorial: Analyze & Visualize": "Návod: Analyzujte a Vizualizujte", + "Tutorial: Create a CRM": "Návod: Vytvorte CRM", + "Tutorial: Manage Business Data": "Návod: Správa obchodných údajov", + "Welcome to the Afterschool Program template": "Vitajte v šablóne Mimoškolský Program", + "Welcome to the Investment Research template": "Vitajte v šablóne Investičný Prieskum", + "Welcome to the Lightweight CRM template": "Vitajte v šablóne Ľahké CRM", + "Check out our related tutorial for how to model business data, use formulas, and manage complexity.": "Pozrite si náš súvisiaci návod, ako modelovať obchodné údaje, používať vzorce a spravovať zložitosť.", + "Check out our related tutorial to learn how to create summary tables and charts, and to link charts dynamically.": "Pozrite si náš súvisiaci návod, v ktorom sa dozviete, ako vytvoriť súhrnné tabuľky a grafy a ako grafy dynamicky prepojiť." + }, + "FieldConfig": { + "COLUMN BEHAVIOR": "SPRÁVANIE STĹPCA", + "COLUMN LABEL AND ID": "ŠTÍTOK A ID STĹPCA", + "Clear and make into formula": "Vyčistiť a vytvoriť vzorec", + "DESCRIPTION": "POPIS", + "Clear and reset": "Vymazať a resetovať", + "Column options are limited in summary tables.": "Možnosti stĺpcov sú v súhrnných tabuľkách obmedzené.", + "Mixed Behavior": "Zmiešané Správanie", + "Empty Columns_other": "Prázdne Stĺpce", + "Convert column to data": "Previesť stĺpec na údaje", + "Convert to trigger formula": "Konvertovať na spúšťací vzorec", + "Data Columns_one": "Stĺpec Údajov", + "Data Columns_other": "Stĺpce Údajov", + "Empty Columns_one": "Prázdny Stĺpec", + "Enter formula": "Zadať vzorec", + "Formula Columns_one": "Stĺpec Vzorca", + "Formula Columns_other": "Stĺpce Vzorca", + "Make into data column": "Vložiť do stĺpca údajov", + "Set trigger formula": "Nastaviť spúšťací vzorec", + "TRIGGER FORMULA": "SPÚŠŤACÍ VZOREC", + "Set formula": "Nastaviť vzorec" + }, + "Drafts": { + "Restore last edit": "Obnoviť poslednú úpravu", + "Undo discard": "Zrušiť zahodenie" + }, + "FieldMenus": { + "Revert to common settings": "Vrátiť sa k bežným nastaveniam", + "Save as common settings": "Uložiť ako bežné nastavenia", + "Use separate settings": "Použiť samostatné nastavenia", + "Using separate settings": "Použitie samostatných nastavení", + "Using common settings": "Použitie bežných nastavení" + }, + "FilterBar": { + "SearchColumns": "Prehľadať stĺpce", + "Search Columns": "Prehľadať Stĺpce" + }, + "GridViewMenus": { + "Add to sort": "Pridať do triedenia", + "Clear values": "Vyčistiť hodnoty", + "Delete {{count}} columns_one": "Odstrániť stĺpec", + "Freeze {{count}} columns_other": "Zmraziť {{count}} stĺpcov", + "Freeze {{count}} more columns_other": "Zmraziť {{count}} ďalšie stĺpce", + "Add Column": "Pridať Stĺpec", + "Column Options": "Možnosti Stĺpca", + "Convert formula to data": "Previesť vzorec na údaje", + "Delete {{count}} columns_other": "Odstrániť {{count}} stĺpcov", + "Filter Data": "Filtrovať Údaje", + "Freeze {{count}} columns_one": "Zmraziť tento stĺpec", + "Freeze {{count}} more columns_one": "Zmraziť ešte jeden stĺpec", + "Hide {{count}} columns_one": "Skryť stĺpec", + "Hide {{count}} columns_other": "Skryť {{count}} stĺpce" + }, + "GridOptions": { + "Horizontal Gridlines": "Horizontálna línia Mriežky", + "Grid Options": "Možnosti Mriežky", + "Vertical Gridlines": "Vertikálna línia Mriežky", + "Zebra Stripes": "Zebra Pruhy" + }, + "FilterConfig": { + "Add Column": "Pridať Stĺpec" } } From 64dc9e13c9b457deea4658278b9918b390c6c3e5 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Mon, 24 Jun 2024 22:39:16 +0000 Subject: [PATCH 08/32] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1336 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index 950fd01c..06457ef5 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -347,7 +347,9 @@ "Reload data engine": "Recarregar o motor de dados", "Reload data engine?": "Recarregar o motor de dados?", "Start timing": "Iniciar cronometragem", - "Stop timing...": "Pare de cronometrar..." + "Stop timing...": "Pare de cronometrar...", + "Only available to document editors": "Disponível apenas para editores de documentos", + "Only available to document owners": "Disponível apenas para proprietários de documentos" }, "DocumentUsage": { "Attachments Size": "Tamanho dos Anexos", From fbdd896f04a30067bfb0cf9305f804b661d7181b Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Mon, 24 Jun 2024 22:39:37 +0000 Subject: [PATCH 09/32] Translated using Weblate (Spanish) Currently translated at 100.0% (1336 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index c8163854..9a54b025 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -289,7 +289,9 @@ "Reload data engine": "Recargar el motor de datos", "Reload data engine?": "¿Recargar motor de datos?", "You can make changes to the document, then stop timing to see the results.": "Puede realizar cambios en el documento y luego detener el cronometraje para ver los resultados.", - "Stop timing...": "Dejando de cronometrar..." + "Stop timing...": "Dejando de cronometrar...", + "Only available to document editors": "Sólo disponible para editores de documentos", + "Only available to document owners": "Solo disponible para los propietarios de documentos" }, "DuplicateTable": { "Copy all data in addition to the table structure.": "Copiar todos los datos además de la estructura de la tabla.", From 3c7623b51b7852eec2d97e9a47855cf91332057c Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Mon, 24 Jun 2024 22:38:27 +0000 Subject: [PATCH 10/32] Translated using Weblate (German) Currently translated at 100.0% (1336 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index af9e532d..f7ed1c6d 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -347,7 +347,9 @@ "Formula timer": "Formel Timer", "Cancel": "Abbrechen", "Timing is on": "Das Timing läuft", - "You can make changes to the document, then stop timing to see the results.": "Sie können Änderungen an dem Dokument vornehmen und dann die Zeitmessung stoppen, um die Ergebnisse zu sehen." + "You can make changes to the document, then stop timing to see the results.": "Sie können Änderungen an dem Dokument vornehmen und dann die Zeitmessung stoppen, um die Ergebnisse zu sehen.", + "Only available to document editors": "Nur für Redakteure von Dokumenten verfügbar", + "Only available to document owners": "Nur für Eigentümer von Dokumenten verfügbar" }, "DocumentUsage": { "Attachments Size": "Größe der Anhänge", From e007b381150e3448a2a80787f2364c5d8b306bdd Mon Sep 17 00:00:00 2001 From: Roman Holinec <3ko@pixeon.sk> Date: Mon, 24 Jun 2024 04:39:52 +0000 Subject: [PATCH 11/32] Translated using Weblate (Slovak) Currently translated at 29.7% (398 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sk/ --- static/locales/sk.client.json | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/static/locales/sk.client.json b/static/locales/sk.client.json index ea46a243..abc89726 100644 --- a/static/locales/sk.client.json +++ b/static/locales/sk.client.json @@ -339,7 +339,9 @@ "Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}": "ID dokumentu, ktoré sa má použiť vždy, keď REST API požaduje {{docId}}. Pozrieť {{apiURL}}", "For currency columns": "Pre stĺpce meny", "Python": "Python", - "Notify other services on doc changes": "Upozorniť ostatné služby na zmeny dokumentu" + "Notify other services on doc changes": "Upozorniť ostatné služby na zmeny dokumentu", + "Only available to document editors": "Dostupné iba pre editorov dokumentov", + "Only available to document owners": "Dostupné iba pre vlastníkov dokumentov" }, "DocumentUsage": { "Attachments Size": "Veľkosť Príloh", @@ -422,7 +424,35 @@ "Freeze {{count}} columns_one": "Zmraziť tento stĺpec", "Freeze {{count}} more columns_one": "Zmraziť ešte jeden stĺpec", "Hide {{count}} columns_one": "Skryť stĺpec", - "Hide {{count}} columns_other": "Skryť {{count}} stĺpce" + "Hide {{count}} columns_other": "Skryť {{count}} stĺpce", + "Reset {{count}} columns_one": "Resetovať stĺpec", + "Reset {{count}} columns_other": "Resetovať {{count}} stĺpcov", + "Reset {{count}} entire columns_one": "Resetovať celý stĺpec", + "Reset {{count}} entire columns_other": "Resetovať {{count}} celé stĺpce", + "Apply to new records": "Použiť na nové záznamy", + "Authorship": "Autorstvo", + "Lookups": "Vyhľadávania", + "Show column {{- label}}": "Zobraziť stĺpec {{- label}}", + "Sort": "Triediť", + "Sorted (#{{count}})_one": "Zoradené (#{{count}})", + "Sorted (#{{count}})_other": "Zoradené (#{{count}})", + "Unfreeze {{count}} columns_one": "Zrušiť zmrazenie tohto stĺpca", + "Unfreeze all columns": "Zrušiť zmrazenie všetkých stĺpcov", + "Show hidden columns": "Zobraziť skryté stĺpce", + "Timestamp": "Časové razítko", + "no reference column": "žiadny referenčný stĺpec", + "Unfreeze {{count}} columns_other": "Zrušiť zmrazenie {{count}} stĺpcov", + "Insert column to the left": "Vložiť stĺpec doľava", + "Insert column to the right": "Vložiť stĺpec doprava", + "Apply on record changes": "Použiť zmeny záznamu", + "Created By": "Vytvoril", + "Hidden Columns": "Skryté stĺpce", + "Last Updated At": "Naposledy aktualizované v", + "Last Updated By": "Naposledy aktualizované používateľom", + "Insert column to the {{to}}": "Vložiť stĺpec do {{to}}", + "More sort options ...": "Ďalšie možnosti zoradenia…", + "Rename column": "Premenovať stĺpec", + "Created At": "Vytvorené v" }, "GridOptions": { "Horizontal Gridlines": "Horizontálna línia Mriežky", From 550c39156b944c705e423b6491ae0969620ce11b Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 25 Jun 2024 17:37:12 +0200 Subject: [PATCH 12/32] Add publiccode.yml (#1056) Co-authored-by: Florent FAYOLLE --- publiccode.yml | 175 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 publiccode.yml diff --git a/publiccode.yml b/publiccode.yml new file mode 100644 index 00000000..df3c5580 --- /dev/null +++ b/publiccode.yml @@ -0,0 +1,175 @@ +# This repository adheres to the publiccode.yml standard by including this +# metadata file that makes public software easily discoverable. +# More info at https://github.com/italia/publiccode.yml + +publiccodeYmlVersion: '0.2' +categories: + - data-collection + - crm + - compliance-management + - office +dependsOn: + open: + - name: NodeJS + optional: false + version: '' + versionMax: '' + versionMin: '18' + - name: Python + optional: false + version: '' + versionMax: '' + versionMin: '3.9' + - name: Yarn + optional: true + version: '' + versionMax: '' + versionMin: '' + - name: Postgresql + optional: true + version: '' + versionMax: '' + versionMin: '' + - name: Redis + optional: true + version: '' + versionMax: '' + versionMin: '' +description: + en: + apiDocumentation: 'https://support.getgrist.com/api/' + documentation: 'https://support.getgrist.com/' + features: + - database + - spreadsheet + - low-code + - no-code + - form generation + - webhook + - calendar + - map + - python formulas + genericName: collaborative spreadsheet + longDescription: | + Grist is a hybrid database/spreadsheet, meaning that: + + - Columns work like they do in databases: they are named, and they hold one kind of data. + - Columns can be filled by formula, spreadsheet-style, with automatic updates when referenced cells change. + + + This difference can confuse people coming directly from Excel or Google + Sheets. Give it a chance! There's also a [Grist for Spreadsheet + Users](https://www.getgrist.com/blog/grist-for-spreadsheet-users/) article + to help get you oriented. If you're coming from Airtable, you'll find the + model familiar (and there's also our [Grist vs + Airtable](https://www.getgrist.com/blog/grist-v-airtable/) article for a + direct comparison). + + Here are some specific feature highlights of Grist: + + - Python formulas. + - Full [Python syntax is supported](https://support.getgrist.com/formulas/#python), including the standard library. + - Many [Excel functions](https://support.getgrist.com/functions/) also available. + - An [AI Assistant](https://www.getgrist.com/ai-formula-assistant/) specifically tuned for formula generation (using OpenAI gpt-3.5-turbo or [Llama](https://ai.meta.com/llama/) via [llama-cpp-python](https://github.com/abetlen/llama-cpp-python)). + - A portable, self-contained format. + - Based on SQLite, the most widely deployed database engine. + - Any tool that can read SQLite can read numeric and text data from a Grist file. + - Enables [backups](https://support.getgrist.com/exports/#backing-up-an-entire-document) that you can confidently restore in full. + - Great for moving between different hosts. + - Can be displayed on a static website with [`grist-static`](https://github.com/gristlabs/grist-static) – no special server needed. + - A self-contained desktop app for viewing and editing locally: [`grist-electron`](https://github.com/gristlabs/grist-electron). + - Convenient editing and formatting features. + - Choices and [choice lists](https://support.getgrist.com/col-types/#choice-list-columns), for adding colorful tags to records. + - [References](https://support.getgrist.com/col-refs/#creating-a-new-reference-list-column) and reference lists, for cross-referencing records in other tables. + - [Attachments](https://support.getgrist.com/col-types/#attachment-columns), to include media or document files in records. + - Dates and times, toggles, and special numerics such as currency all have specialized editors and formatting options. + - [Conditional Formatting](https://support.getgrist.com/conditional-formatting/), letting you control the style of cells with formulas to draw attention to important information. + - Drag-and-drop dashboards. + - [Charts](https://support.getgrist.com/widget-chart/), [card views](https://support.getgrist.com/widget-card/) and a [calendar widget](https://support.getgrist.com/widget-calendar/) for visualization. + - [Summary tables](https://support.getgrist.com/summary-tables/) for summing and counting across groups. + - [Widget linking](https://support.getgrist.com/linking-widgets/) streamlines filtering and editing data. Grist has a unique approach to visualization, where you can lay out and link distinct widgets to show together, without cramming mixed material into a table. + - [Filter bar](https://support.getgrist.com/search-sort-filter/#filter-buttons) for quick slicing and dicing. + - [Incremental imports](https://support.getgrist.com/imports/#updating-existing-records). + - Import a CSV of the last three months activity from your bank... + - ...and import new activity a month later without fuss or duplication. + - Integrations. + - A [REST API](https://support.getgrist.com/api/), [Zapier actions/triggers](https://support.getgrist.com/integrators/#integrations-via-zapier), and support from similar [integrators](https://support.getgrist.com/integrators/). + - Import/export to Google drive, Excel format, CSV. + - Link data with [custom widgets](https://support.getgrist.com/widget-custom/#_top), hosted externally. + - Configurable outgoing webhooks. + - [Many templates](https://templates.getgrist.com/) to get you started, from investment research to organizing treasure hunts. + - Access control options. + - (You'll need SSO logins set up to make use of these options; [`grist-omnibus`](https://github.com/gristlabs/grist-omnibus) has a prepackaged solution if configuring this feels daunting) + - Share [individual documents](https://support.getgrist.com/sharing/), workspaces, or [team sites](https://support.getgrist.com/team-sharing/). + - Control access to [individual rows, columns, and tables](https://support.getgrist.com/access-rules/). + - Control access based on cell values and user attributes. + - Self-maintainable. + - Useful for intranet operation and specific compliance requirements. + - Sandboxing options for untrusted documents. + - On Linux or with Docker, you can enable [gVisor](https://github.com/google/gvisor) sandboxing at the individual document level. + - On macOS, you can use native sandboxing. + - On any OS, including Windows, you can use a wasm-based sandbox. + - Translated to many languages. + - `F1` key brings up some quick help. This used to go without saying, but in general Grist has good keyboard support. + shortDescription: |- + Grist is a modern relational spreadsheet. It combines the flexibility of a + + spreadsheet with the robustness of a database. + videos: + - 'https://www.youtube.com/watch?v=XYZ_ZGSxU00' +developmentStatus: stable +inputTypes: + - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + - text/csv +it: + conforme: + gdpr: false + lineeGuidaDesign: false + misureMinimeSicurezza: false + modelloInteroperabilita: false + countryExtensionVersion: '0.2' + piattaforme: + anpr: false + cie: false + pagopa: false + spid: false +landingURL: 'https://getgrist.com' +legal: + license: Apache-2.0 +localisation: + availableLanguages: + - en + - fr + - ru + - de + - es + - pt + - zh + - it + - ja + - 'no' + - ro + - sl + - uk + localisationReady: true +logo: |- + https://raw.githubusercontent.com/gristlabs/grist-core/master/static/img/logo-grist.png +maintenance: + contacts: + - affiliation: Grist Labs + email: paul@getgrist.com + name: Paul Fitzpatrick + type: internal +name: Grist +outputTypes: + - application/x-sqlite3 +platforms: + - web +releaseDate: '2024-06-12' +roadmap: 'https://github.com/gristlabs/grist-core/projects/1' +softwareType: standalone/other +softwareVersion: 1.1.15 +url: 'https://github.com/gristlabs/grist-core' +usedBy: + - 'ANCT (https://anct.gouv.fr)' + - 'DINUM (https://www.numerique.gouv.fr/dinum/)' From 24ce54b586e20a260376a9e3d5b6774e3fa2b8b8 Mon Sep 17 00:00:00 2001 From: Leslie H <142967379+SleepyLeslie@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:43:25 -0400 Subject: [PATCH 13/32] Improve session ID security (#1059) Follow-up of #994. This PR revises the session ID generation logic to improve security in the absence of a secure session secret. It also adds a section in the admin panel "security" section to nag system admins when GRIST_SESSION_SECRET is not set. Following is an excerpt from internal conversation. TL;DR: Grist's current implementation generates semi-secure session IDs and uses a publicly known default signing key to sign them when the environment variable GRIST_SESSION_SECRET is not set. This PR generates cryptographically secure session IDs to dismiss security concerns around an insecure signing key, and encourages system admins to configure their own signing key anyway. > The session secret is required by expressjs/session to sign its session IDs. It's designed as an extra protection against session hijacking by randomly guessing session IDs and hitting a valid one. While it is easy to encourage users to set a distinct session secret, this is unnecessary if session IDs are generated in a cryptographically secure way. As of now Grist uses version 4 UUIDs as session IDs (see app/server/lib/gristSessions.ts - it uses shortUUID.generate which invokes uuid.v4 under the hood). These contain 122 bits of entropy, technically insufficient to be considered cryptographically secure. In practice, this is never considered a real vulnerability. To compare, RSA2048 is still very commonly used in web servers, yet it only has 112 bits of security (>=128 bits = "secure", rule of thumb in cryptography). But for peace of mind I propose using crypto.getRandomValues to generate real 128-bit random values. This should render session ID signing unnecessary and hence dismiss security concerns around an insecure signing key. --- app/client/ui/AdminPanel.ts | 34 ++++++++++++++++++++++++++++++++- app/common/BootProbe.ts | 3 ++- app/server/lib/BootProbes.ts | 16 ++++++++++++++++ app/server/lib/coreCreator.ts | 5 ++++- app/server/lib/gristSessions.ts | 7 +++++-- static/locales/en.client.json | 1 + 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/app/client/ui/AdminPanel.ts b/app/client/ui/AdminPanel.ts index bfd2bd6c..91f6802b 100644 --- a/app/client/ui/AdminPanel.ts +++ b/app/client/ui/AdminPanel.ts @@ -145,6 +145,13 @@ Please log in as an administrator.`)), description: t('Current authentication method'), value: this._buildAuthenticationDisplay(owner), expandedContent: this._buildAuthenticationNotice(owner), + }), + dom.create(AdminSectionItem, { + id: 'session', + name: t('Session Secret'), + description: t('Key to sign sessions with'), + value: this._buildSessionSecretDisplay(owner), + expandedContent: this._buildSessionSecretNotice(owner), }) ]), dom.create(AdminSection, t('Version'), [ @@ -241,6 +248,27 @@ We recommend enabling one of these if Grist is accessible over the network or be to multiple people.'); } + private _buildSessionSecretDisplay(owner: IDisposableOwner) { + return dom.domComputed( + use => { + const req = this._checks.requestCheckById(use, 'session-secret'); + const result = req ? use(req.result) : undefined; + + if (result?.status === 'warning') { + return cssValueLabel(cssDangerText('default')); + } + + return cssValueLabel(cssHappyText('configured')); + } + ); + } + + private _buildSessionSecretNotice(owner: IDisposableOwner) { + return t('Grist signs user session cookies with a secret key. Please set this key via the environment variable \ +GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice \ +in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.'); + } + private _buildUpdates(owner: MultiHolder) { // We can be in those states: enum State { @@ -472,7 +500,11 @@ to multiple people.'); return dom.domComputed( use => [ ...use(this._checks.probes).map(probe => { - const isRedundant = probe.id === 'sandboxing'; + const isRedundant = [ + 'sandboxing', + 'authentication', + 'session-secret' + ].includes(probe.id); const show = isRedundant ? options.showRedundant : options.showNovel; if (!show) { return null; } const req = this._checks.requestCheck(probe); diff --git a/app/common/BootProbe.ts b/app/common/BootProbe.ts index 49fb9911..c73843b1 100644 --- a/app/common/BootProbe.ts +++ b/app/common/BootProbe.ts @@ -8,7 +8,8 @@ export type BootProbeIds = 'sandboxing' | 'system-user' | 'authentication' | - 'websockets' + 'websockets' | + 'session-secret' ; export interface BootProbeResult { diff --git a/app/server/lib/BootProbes.ts b/app/server/lib/BootProbes.ts index 61ac66eb..adef4811 100644 --- a/app/server/lib/BootProbes.ts +++ b/app/server/lib/BootProbes.ts @@ -6,6 +6,7 @@ import { GristServer } from 'app/server/lib/GristServer'; import * as express from 'express'; import WS from 'ws'; import fetch from 'node-fetch'; +import { DEFAULT_SESSION_SECRET } from 'app/server/lib/coreCreator'; /** * Self-diagnostics useful when installing Grist. @@ -61,6 +62,7 @@ export class BootProbes { this._probes.push(_sandboxingProbe); this._probes.push(_authenticationProbe); this._probes.push(_webSocketsProbe); + this._probes.push(_sessionSecretProbe); this._probeById = new Map(this._probes.map(p => [p.id, p])); } } @@ -284,3 +286,17 @@ const _authenticationProbe: Probe = { }; }, }; + +const _sessionSecretProbe: Probe = { + id: 'session-secret', + name: 'Session secret', + apply: async(server, req) => { + const usingDefaultSessionSecret = server.create.sessionSecret() === DEFAULT_SESSION_SECRET; + return { + status: usingDefaultSessionSecret ? 'warning' : 'success', + details: { + "GRIST_SESSION_SECRET": process.env.GRIST_SESSION_SECRET ? "set" : "not set", + } + }; + }, +}; diff --git a/app/server/lib/coreCreator.ts b/app/server/lib/coreCreator.ts index f4536c16..477c970b 100644 --- a/app/server/lib/coreCreator.ts +++ b/app/server/lib/coreCreator.ts @@ -3,11 +3,14 @@ import { checkMinIOBucket, checkMinIOExternalStorage, import { makeSimpleCreator } from 'app/server/lib/ICreate'; import { Telemetry } from 'app/server/lib/Telemetry'; +export const DEFAULT_SESSION_SECRET = + 'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh'; + export const makeCoreCreator = () => makeSimpleCreator({ deploymentType: 'core', // This can and should be overridden by GRIST_SESSION_SECRET // (or generated randomly per install, like grist-omnibus does). - sessionSecret: 'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh', + sessionSecret: DEFAULT_SESSION_SECRET, storage: [ { name: 'minio', diff --git a/app/server/lib/gristSessions.ts b/app/server/lib/gristSessions.ts index 987fae58..a5555ba1 100644 --- a/app/server/lib/gristSessions.ts +++ b/app/server/lib/gristSessions.ts @@ -6,10 +6,10 @@ import {GristServer} from 'app/server/lib/GristServer'; import {fromCallback} from 'app/server/lib/serverUtils'; import {Sessions} from 'app/server/lib/Sessions'; import {promisifyAll} from 'bluebird'; +import * as crypto from 'crypto'; import * as express from 'express'; import assignIn = require('lodash/assignIn'); import * as path from 'path'; -import * as shortUUID from "short-uuid"; export const cookieName = process.env.GRIST_SESSION_COOKIE || 'grist_sid'; @@ -118,7 +118,10 @@ export function initGristSessions(instanceRoot: string, server: GristServer) { // cookie could be stolen (with some effort) by the custom domain's owner, we limit the damage // by only honoring custom-domain cookies for requests to that domain. const generateId = (req: RequestWithOrg) => { - const uid = shortUUID.generate(); + // Generate 256 bits of cryptographically random data to use as the session ID. + // This ensures security against brute-force session hijacking even without signing the session ID. + const randomNumbers = crypto.getRandomValues(new Uint8Array(32)); + const uid = Buffer.from(randomNumbers).toString("hex"); return req.isCustomHost ? `c-${uid}@${req.org}@${req.get('host')}` : `g-${uid}`; }; const sessionSecret = server.create.sessionSecret(); diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 7a8ee8df..baae4538 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1541,6 +1541,7 @@ "Error": "Error", "Error checking for updates": "Error checking for updates", "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.": "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.", "Grist is up to date": "Grist is up to date", "Grist releases are at ": "Grist releases are at ", "Last checked {{time}}": "Last checked {{time}}", From c0e4cea273bffd7b9b20dd273b0efbb5eb13fe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C4=8Dek=20Prijatelj?= Date: Tue, 25 Jun 2024 10:12:02 +0000 Subject: [PATCH 14/32] Translated using Weblate (Slovenian) Currently translated at 100.0% (1336 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sl/ --- static/locales/sl.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/sl.client.json b/static/locales/sl.client.json index f359c24b..7eeb44eb 100644 --- a/static/locales/sl.client.json +++ b/static/locales/sl.client.json @@ -538,7 +538,9 @@ "Cancel": "Prekliči", "Force reload the document while timing formulas, and show the result.": "Prisilno znova naloži dokument med časovnimi formulami in prikaži rezultat.", "Timing is on": "Merjenje časa je vklopljeno", - "You can make changes to the document, then stop timing to see the results.": "Dokument lahko spremeniš in nato ustaviš merjenje časa, da vidiš rezultat." + "You can make changes to the document, then stop timing to see the results.": "Dokument lahko spremeniš in nato ustaviš merjenje časa, da vidiš rezultat.", + "Only available to document editors": "Na voljo samo urednikom dokumentov", + "Only available to document owners": "Na voljo samo lastnikom dokumentov" }, "GridOptions": { "Horizontal Gridlines": "Vodoravne linije", From eed5f364c07dd5793fa389909ca86d6b857f3d8c Mon Sep 17 00:00:00 2001 From: Roman Holinec <3ko@pixeon.sk> Date: Tue, 25 Jun 2024 18:28:19 +0000 Subject: [PATCH 15/32] Translated using Weblate (Slovak) Currently translated at 37.1% (496 of 1336 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sk/ --- static/locales/sk.client.json | 112 +++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/static/locales/sk.client.json b/static/locales/sk.client.json index abc89726..b5b72d72 100644 --- a/static/locales/sk.client.json +++ b/static/locales/sk.client.json @@ -452,7 +452,35 @@ "Insert column to the {{to}}": "Vložiť stĺpec do {{to}}", "More sort options ...": "Ďalšie možnosti zoradenia…", "Rename column": "Premenovať stĺpec", - "Created At": "Vytvorené v" + "Created At": "Vytvorené v", + "Detect Duplicates in...": "Zistiť duplikáty v...", + "Duplicate in {{- label}}": "Duplikovať v {{- label}}", + "Shortcuts": "Skratky", + "Adding UUID column": "Pridávať stĺpec UUID", + "Adding duplicates column": "Pridávať stĺpec duplikátov", + "Search columns": "Prehľadať stĺpce", + "UUID": "UUID", + "No reference columns.": "Žiadne referenčné stĺpce.", + "Add formula column": "Pridať stĺpec vzorca", + "Add column with type": "Pridať stĺpec s typom", + "Add column": "Pridať stĺpec", + "Created at": "Vytvorené v", + "Created by": "Vytvoril", + "Detect duplicates in...": "Zistiť duplikáty v...", + "DateTime": "Dátum a Čas", + "Choice": "Voľba", + "Reference": "Referencia", + "Last updated at": "Naposledy aktualizované v", + "Last updated by": "Naposledy aktualizoval", + "Date": "Dátum", + "Any": "Akýkoľvek", + "Text": "Text", + "Integer": "Celé číslo", + "Numeric": "Desatinné číslo", + "Toggle": "Prepínať", + "Choice List": "Výberový zoznam", + "Reference List": "Zoznam referencií", + "Attachment": "Príloha" }, "GridOptions": { "Horizontal Gridlines": "Horizontálna línia Mriežky", @@ -462,5 +490,87 @@ }, "FilterConfig": { "Add Column": "Pridať Stĺpec" + }, + "HomeIntro": { + "Welcome to {{- orgName}}": "Vitajte v {{- orgName}}", + "{{signUp}} to save your work. ": "Ak chcete uložiť svoju prácu, {{signUp}}. ", + "Welcome to Grist, {{- name}}!": "Vitajte v Grist, {{- name}}!", + "Any documents created in this site will appear here.": "Všetky dokumenty vytvorené na tejto stránke sa zobrazia tu.", + "Browse Templates": "Prechádzať Šablóny", + "Create Empty Document": "Vytvoriť Prázdny Dokument", + "Get started by creating your first Grist document.": "Začať vytvorením svojho prvého dokumentu Grist.", + "Get started by exploring templates, or creating your first Grist document.": "Začať skúmaním šablón, alebo vytvorením prvého dokumentu Grist.", + "Get started by inviting your team and creating your first Grist document.": "Začať tým, že pozvete svoj tím a vytvoríte svoj prvý dokument Grist.", + "Help Center": "Centrum Pomoci", + "Import Document": "Importovať Dokument", + "Invite Team Members": "Pozvať Členov Tímu", + "This workspace is empty.": "Tento pracovný priestor je prázdny.", + "Sprouts Program": "Program Klíčenia", + "Visit our {{link}} to learn more.": "Viac informácií na našej stránke {{link}}.", + "Welcome to Grist!": "Vitajte v Grist!", + "Welcome to Grist, {{name}}!": "Vitajte v Grist, {{name}}!", + "Welcome to {{orgName}}": "Vitajte v {{orgName}}", + "You have read-only access to this site. Currently there are no documents.": "K tejto lokalite máte prístup iba na čítanie. V súčasnosti neexistujú žiadne dokumenty.", + "personal site": "osobná stránka", + "Sign in": "Prihlásiť sa", + "To use Grist, please either sign up or sign in.": "Ak chcete používať Grist, zaregistrujte sa alebo sa prihláste.", + "Visit our {{link}} to learn more about Grist.": "Navštíviť náš {{link}} a dozvedieť sa viac o Grist.", + "Learn more in our {{helpCenterLink}}, or find an expert via our {{sproutsProgram}}.": "Viac informácií nájdete v našom {{helpCenterLink}} alebo nájdite odborníka prostredníctvom nášho {{sproutsProgram}}.", + "Interested in using Grist outside of your team? Visit your free ": "Máte záujem používať Grist mimo váš tím? Navštívte svoje bezplatné ", + "Sign up": "Prihlásiť sa" + }, + "Importer": { + "Merge rows that match these fields:": "Zlúčiť riadky, ktoré zodpovedajú týmto poliam:", + "Select fields to match on": "Vyberte polia podľa obsahu", + "Update existing records": "Aktualizovať existujúce záznamy", + "{{count}} unmatched field in import_one": "{{count}} nezhodné pole v importe", + "{{count}} unmatched field in import_other": "{{count}} nezhodných polí pri importe", + "{{count}} unmatched field_one": "{{count}} nezhodné pole", + "{{count}} unmatched field_other": "{{count}} nezhodné polia", + "Column Mapping": "Mapovanie Stĺpcov", + "Column mapping": "Mapovanie stĺpcov", + "Destination table": "Cieľová tabuľka", + "Grist column": "Grist stĺpec", + "Import from file": "Importovať zo súboru", + "New Table": "Nová Tabuľka", + "Revert": "Návrat", + "Skip": "Preskočiť", + "Skip Import": "Preskočiť import", + "Skip Table on Import": "Preskočiť Tabuľku pri Importe", + "Source column": "Zdrojový stĺpec" + }, + "MakeCopyMenu": { + "As Template": "Ako Šablóna", + "Enter document name": "Zadajte názov dokumentu", + "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Buďte opatrní, originál obsahuje zmeny, ktoré nie sú v tomto dokumente. Tieto zmeny budú prepísané.", + "However, it appears to be already identical.": "Zdá sa však, že je už identický.", + "Include the structure without any of the data.": "Zahrňte štruktúru bez akýchkoľvek údajov.", + "Cancel": "Zrušiť" + }, + "GristDoc": { + "Added new linked section to view {{viewName}}": "Pridaná nová prepojená sekcia na zobrazenie {{viewName}}", + "Import from file": "Importovať zo súboru", + "Saved linked section {{title}} in view {{name}}": "Uložená prepojená sekcia {{title}} v zobrazení {{name}}", + "go to webhook settings": "prejsť na nastavenia webhooku" + }, + "HomeLeftPane": { + "Access Details": "Podrobnosti Prístupu", + "All Documents": "Všetky Dokumenty", + "Create Empty Document": "Vytvoriť Prázdny Dokument", + "Create Workspace": "Vytvoriť Pracovný priestor", + "Delete": "Odstrániť", + "Delete {{workspace}} and all included documents?": "Odstrániť {{workspace}} a všetky zahrnuté dokumenty?", + "Examples & Templates": "Šablóny", + "Import Document": "Importovať Dokument", + "Manage Users": "Spravovať Používateľov", + "Rename": "Premenovať", + "Trash": "Odpad", + "Workspace will be moved to Trash.": "Pracovný priestor sa presunie do Koša.", + "Workspaces": "Pracovné priestory", + "Terms of service": "Podmienky služby", + "Tutorial": "Návod" + }, + "LeftPanelCommon": { + "Help Center": "Centrum Pomoci" } } From 187358cfa26565a45dcc9d7207b978ea16b1fac6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:36:15 -0400 Subject: [PATCH 16/32] automated update to translation keys (#1065) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index baae4538..420e580e 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1568,7 +1568,10 @@ "Results": "Results", "Self Checks": "Self Checks", "You do not have access to the administrator panel.\nPlease log in as an administrator.": "You do not have access to the administrator panel.\nPlease log in as an administrator.", - "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people." + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.", + "Key to sign sessions with": "Key to sign sessions with", + "Session Secret": "Session Secret" }, "Columns": { "Remove Column": "Remove Column" From 3b3aa8a86e922b2f5409da86ca0753f3a048837c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 19 Jun 2024 12:17:48 -0400 Subject: [PATCH 17/32] build.sh: add some diagnostic output As I was testing, I found it useful to see when I was using the ext/ directory or not. --- buildtools/build.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/buildtools/build.sh b/buildtools/build.sh index c87643a6..4c66a345 100755 --- a/buildtools/build.sh +++ b/buildtools/build.sh @@ -5,7 +5,11 @@ set -e PROJECT="" if [[ -e ext/app ]]; then PROJECT="tsconfig-ext.json" + echo "Using extra app directory" +else + echo "No extra app directory found" fi + WEBPACK_CONFIG=buildtools/webpack.config.js if [[ -e ext/buildtools/webpack.config.js ]]; then # Allow webpack config file to be replaced (useful From 36f897fd359dab95bbb053557ecb239b1d7c7cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 20 Jun 2024 16:36:03 -0400 Subject: [PATCH 18/32] .grist-ee-version: start referencing the intended enterprise version Since we won't be tracking ext/-directory providers via git (e.g. no submodules), instead we'll do little version-tracking files like this, to be used by the recent ext-checkout script. --- buildtools/.grist-ee-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 buildtools/.grist-ee-version diff --git a/buildtools/.grist-ee-version b/buildtools/.grist-ee-version new file mode 100644 index 00000000..2003b639 --- /dev/null +++ b/buildtools/.grist-ee-version @@ -0,0 +1 @@ +0.9.2 From bd7b7b778b6e6abc7df634a43f0c3cb11618989d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 19 Jun 2024 12:18:34 -0400 Subject: [PATCH 19/32] checkout-ext-directory: new helper script This is just a helper script to get the ext directory of other grist repos, currently intended for grist-ee. --- buildtools/checkout-ext-directory.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 buildtools/checkout-ext-directory.sh diff --git a/buildtools/checkout-ext-directory.sh b/buildtools/checkout-ext-directory.sh new file mode 100755 index 00000000..6861b9e2 --- /dev/null +++ b/buildtools/checkout-ext-directory.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# This checks out the ext/ directory from the extra repo (e.g. +# grist-ee or grist-desktop) depending on the supplied repo name. + +set -e + +repo=$1 +dir=$(dirname $0) +ref=$(cat $dir/.$repo-version) + +git clone --branch $ref --depth 1 --filter=tree:0 "https://github.com/gristlabs/$repo" +pushd $repo +git sparse-checkout set ext +git checkout +popd +mv $repo/ext . +rm -rf $repo From bc52f65b2648ff7987383537fc06c6a388d29ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 19 Jun 2024 14:27:43 -0400 Subject: [PATCH 20/32] tsconfig-ext: add /app, /test, and /stubs/app directories This is so that they get built and tested, as we'll start running tests on the ext/ directories from now on. --- tsconfig-ext.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig-ext.json b/tsconfig-ext.json index 814d5c36..cfa355ab 100644 --- a/tsconfig-ext.json +++ b/tsconfig-ext.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { "path": "./app" }, + { "path": "./stubs/app" }, + { "path": "./test" }, { "path": "./ext/app" } ], } From 70ed8553b3a58b5607789627f5386037d0101362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Fri, 21 Jun 2024 17:55:30 -0400 Subject: [PATCH 21/32] workflows: build the grist-oss, grist, and grist-ee images This modifies the workflow to build grist-ee images as well as grist, which is the same image as grist-ee but merely renamed. The original image built by these workflows is now called grist-oss. --- .github/workflows/docker.yml | 27 ++++++++++++++--- .github/workflows/docker_latest.yml | 46 +++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ef1f5d7b..3ffafb53 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,17 +8,31 @@ on: jobs: push_to_registry: - name: Push Docker image to Docker Hub + name: Push Docker images to Docker Hub runs-on: ubuntu-latest + strategy: + matrix: + image: + - name: "grist-oss" + repo: "grist-core" + - name: "grist" + repo: "grist-ee" + - name: "grist-ee" + repo: "grist-ee" steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Check out the ext/ directory + if: matrix.image.name != 'grist-oss' + run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }} + - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: | - ${{ github.repository_owner }}/grist + ${{ github.repository_owner }}/${{ matrix.image.name }} tags: | type=ref,event=branch type=ref,event=pr @@ -28,13 +42,16 @@ jobs: stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 + - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Push to Docker Hub uses: docker/build-push-action@v2 with: @@ -44,3 +61,5 @@ jobs: tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max + build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }} + diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index 5b79ef03..e6cea6f8 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -10,6 +10,12 @@ on: # Run at 5:41 UTC daily - cron: '41 5 * * *' workflow_dispatch: + inputs: + latest_branch: + description: Branch from which to create the latest Docker image (default: latest_candidate) + type: string + required: true + default_value: latest_candidate jobs: push_to_registry: @@ -19,54 +25,84 @@ jobs: matrix: python-version: [3.9] node-version: [18.x] + image: + - name: "grist-oss" + repo: "grist-core" + - name: "grist" + repo: "grist-ee" + - name: "grist-ee" + repo: "grist-ee" steps: - name: Check out the repo uses: actions/checkout@v2 with: - ref: latest_candidate + ref: ${{ inputs.latest_branch }} + + - name: Check out the ext/ directory + if: matrix.image.name != 'grist-oss' + run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }} + + - name: Set up QEMU uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 + - name: Prepare image but do not push it yet uses: docker/build-push-action@v2 with: context: . load: true - tags: ${{ github.repository_owner }}/grist:latest + tags: ${{ github.repository_owner }}/${{ matrix.image.name }}:experimental cache-from: type=gha + build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }} + - name: Use Node.js ${{ matrix.node-version }} for testing uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Set up Python ${{ matrix.python-version }} for testing - maybe not needed uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install Python packages run: | pip install virtualenv yarn run install:python + - name: Install Node.js packages run: yarn install + - name: Build Node.js code - run: yarn run build:prod + run: | + pushd ext && \ + { if [ -e package.json ] ; then yarn install --frozen-lockfile --modules-folder=../../node_modules; fi } && \ + popd + yarn run build:prod + - name: Run tests - run: TEST_IMAGE=${{ github.repository_owner }}/grist VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker + run: TEST_IMAGE=${{ github.repository_owner }}/${{ matrix.image.name }} VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker + - name: Log in to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Push to Docker Hub uses: docker/build-push-action@v2 with: context: . platforms: linux/amd64,linux/arm64/v8 push: true - tags: ${{ github.repository_owner }}/grist:latest + tags: ${{ github.repository_owner }}/${{ matrix.image.name }}:experimental cache-from: type=gha cache-to: type=gha,mode=max + build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }} + - name: Update latest branch uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 with: From 8cd9e40744d7ed6801daab5991039cf8a0472850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Fri, 21 Jun 2024 17:56:07 -0400 Subject: [PATCH 22/32] README: Mention the two possible docker images --- .github/workflows/docker.yml | 4 ++++ .github/workflows/docker_latest.yml | 4 ++++ README.md | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3ffafb53..f80c204c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,10 +13,14 @@ jobs: strategy: matrix: image: + # We build two images, `grist-oss` and `grist`. + # See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images - name: "grist-oss" repo: "grist-core" - name: "grist" repo: "grist-ee" + # For now, we build it twice, with `grist-ee` being a + # backwards-compatible synoym for `grist`. - name: "grist-ee" repo: "grist-ee" steps: diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index e6cea6f8..e105f01e 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -26,10 +26,14 @@ jobs: python-version: [3.9] node-version: [18.x] image: + # We build two images, `grist-oss` and `grist`. + # See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images - name: "grist-oss" repo: "grist-core" - name: "grist" repo: "grist-ee" + # For now, we build it twice, with `grist-ee` being a + # backwards-compatible synoym for `grist`. - name: "grist-ee" repo: "grist-ee" steps: diff --git a/README.md b/README.md index ee9099d9..1f4f1b4e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ If you just want a quick demo of Grist: * Or you can see a fully in-browser build of Grist at [gristlabs.github.io/grist-static](https://gristlabs.github.io/grist-static/). * Or you can download Grist as a desktop app from [github.com/gristlabs/grist-desktop](https://github.com/gristlabs/grist-desktop). -To get `grist-core` running on your computer with [Docker](https://www.docker.com/get-started), do: +To get the default version of `grist-core` running on your computer +with [Docker](https://www.docker.com/get-started), do: ```sh docker pull gristlabs/grist @@ -117,6 +118,22 @@ You can find a lot more about configuring Grist, setting up authentication, and running it on a public server in our [Self-Managed Grist](https://support.getgrist.com/self-managed/) handbook. +## Available Docker images + +The default Docker image is `gristlabs/grist`. This contains all of +the standard Grist functionality, as well as extra source-available +code for enterprise customers taken from the the +[grist-ee](https://github.com/gristlabs/grist-ee) repository. This +extra code is not under a free or open source license. By default, +however, the code from the `grist-ee` repository is completely inert and +inactive. This code becomes active only when an administrator enables +it by setting either `GRIST_ACTIVATION` or `GRIST_ACTIVATION_FILE`. + +If you would rather use an image that contains exclusively free and +open source code, the `gristlabs/grist-oss` Docker image is available +for this purpose. It is by default functionally equivalent to the +`gristlabs/grist` image. + ## The administrator panel You can turn on a special admininistrator panel to inspect the status From 40f7060ac5c4e8691d3d5da4d78c8ffd56925371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 26 Jun 2024 15:24:47 -0400 Subject: [PATCH 23/32] workflows: fix syntax error Oops, have to quote the string because of the colon --- .github/workflows/docker_latest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index e105f01e..e5b7a5b8 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -12,7 +12,7 @@ on: workflow_dispatch: inputs: latest_branch: - description: Branch from which to create the latest Docker image (default: latest_candidate) + description: "Branch from which to create the latest Docker image (default: latest_candidate)" type: string required: true default_value: latest_candidate From a8431c69a735bf54df80a81134ca059bd8f29c5e Mon Sep 17 00:00:00 2001 From: Spoffy <4805393+Spoffy@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:24:32 +0100 Subject: [PATCH 24/32] Makes docker images default to non-root execution (#1031) De-escalates to a normal user when the docker image is run as root. Allows GRIST_DOCKER_USER and GRIST_DOCKER_GROUP to be passed to override the default de-escalation behaviour. Backwards compatible with previous root installations. -------- This change adds a new docker_entrypoint.sh, which when run as root de-escalates to the provided user, defaulting to grist:grist. This is similar to the approach used by the official postgres docker image. To achieve backwards compatibility, it changes ownership of any files in `/persist` to the user it's given at runtime. Since the docker container is typically run as root, this should always work. If the container is run as a standard user from the very start: * It's the admin's responsibility to ensure `/persist` is writable by that user. * `/grist` remains owned by root and is read-only. --- Dockerfile | 11 +++++++++- sandbox/docker_entrypoint.sh | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100755 sandbox/docker_entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 35148cdb..cdd584f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -122,6 +122,15 @@ RUN \ mv /grist/static-built/* /grist/static && \ rmdir /grist/static-built +# To ensure non-root users can run grist, 'other' users need read access (and execute on directories) +# This should be the case by default when copying files in. +# Only uncomment this if running into permissions issues, as it takes a long time to execute on some systems. +# RUN chmod -R o+rX /grist + +# Add a user to allow de-escalating from root on startup +RUN useradd -ms /bin/bash grist +ENV GRIST_DOCKER_USER=grist \ + GRIST_DOCKER_GROUP=grist WORKDIR /grist # Set some default environment variables to give a setup that works out of the box when @@ -151,5 +160,5 @@ ENV \ EXPOSE 8484 -ENTRYPOINT ["/usr/bin/tini", "-s", "--"] +ENTRYPOINT ["./sandbox/docker_entrypoint.sh"] CMD ["node", "./sandbox/supervisor.mjs"] diff --git a/sandbox/docker_entrypoint.sh b/sandbox/docker_entrypoint.sh new file mode 100755 index 00000000..7072e07e --- /dev/null +++ b/sandbox/docker_entrypoint.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Runs the command provided as arguments, but attempts to configure permissions first. + +important_read_dirs=("/grist" "/persist") +write_dir="/persist" +current_user_id=$(id -u) + +# We want to avoid running Grist as root if possible. +# Try to setup permissions and de-elevate to a normal user. +if [[ $current_user_id == 0 ]]; then + target_user=${GRIST_DOCKER_USER:-grist} + target_group=${GRIST_DOCKER_GROUP:-grist} + + # Make sure the target user owns everything that Grist needs write access to. + find $write_dir ! -user "$target_user" -exec chown "$target_user" "{}" + + + # Restart as the target user, replacing the current process (replacement is needed for security). + # Alternative tools to setpriv are: chroot, gosu. + # Need to use `exec` to close the parent shell, to avoid vulnerabilities: https://github.com/tianon/gosu/issues/37 + exec setpriv --reuid "$target_user" --regid "$target_group" --init-groups /usr/bin/env bash "$0" "$@" +fi + +# Validate that this user has access to the top level of each important directory. +# There might be a benefit to testing individual files, but this is simpler as the dir may start empty. +for dir in "${important_read_dirs[@]}"; do + if ! { test -r "$dir" ;} ; then + echo "Invalid permissions, cannot read '$dir'. Aborting." >&2 + exit 1 + fi +done +for dir in "${important_write_dirs[@]}"; do + if ! { test -r "$dir" && test -w "$dir" ;} ; then + echo "Invalid permissions, cannot write '$dir'. Aborting." >&2 + exit 1 + fi +done + +exec /usr/bin/tini -s -- "$@" From 1e5cc585a7b4a69954bcfbd09e76641c007953d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Wed, 26 Jun 2024 17:02:45 -0400 Subject: [PATCH 25/32] workflows: update the latest branch conditionally Since we now run the build three times, we don't want to update the latest branch unless all three builds complete successfully. --- .github/workflows/docker_latest.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index e5b7a5b8..d4539031 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -107,6 +107,16 @@ jobs: cache-to: type=gha,mode=max build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }} + update_latest_branch: + name: Update latest branch + runs-on: ubuntu-latest + needs: push_to_registry + steps: + - name: Check out the repo + uses: actions/checkout@v2 + with: + ref: ${{ inputs.latest_branch }} + - name: Update latest branch uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 with: From cefadc50c4c7a2a3b32b9cb91edcf45ef4cf6eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 27 Jun 2024 08:34:19 -0400 Subject: [PATCH 26/32] workflows: ensure we also use the experimental image we just built I think without a tag it defaults to `latest`, which is not what we want. --- .github/workflows/docker_latest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index d4539031..4a6f2c1c 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -33,7 +33,7 @@ jobs: - name: "grist" repo: "grist-ee" # For now, we build it twice, with `grist-ee` being a - # backwards-compatible synoym for `grist`. + # backwards-compatible synonym for `grist`. - name: "grist-ee" repo: "grist-ee" steps: @@ -88,7 +88,7 @@ jobs: yarn run build:prod - name: Run tests - run: TEST_IMAGE=${{ github.repository_owner }}/${{ matrix.image.name }} VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker + run: TEST_IMAGE=${{ github.repository_owner }}/${{ matrix.image.name }}:experimental VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker - name: Log in to Docker Hub uses: docker/login-action@v1 From 6e9dae291c8c251f539d7d9532c6fa05b19627aa Mon Sep 17 00:00:00 2001 From: Riccardo Polignieri Date: Fri, 28 Jun 2024 20:47:06 +0000 Subject: [PATCH 27/32] Translated using Weblate (Italian) Currently translated at 100.0% (1337 of 1337 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/it/ --- static/locales/it.client.json | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/static/locales/it.client.json b/static/locales/it.client.json index c252e752..bdae0869 100644 --- a/static/locales/it.client.json +++ b/static/locales/it.client.json @@ -41,7 +41,8 @@ "Trash": "Cestino", "Workspace will be moved to Trash.": "Lo spazio di lavoro sarà spostato nel cestino.", "Workspaces": "Spazi di lavoro", - "Tutorial": "Tutorial" + "Tutorial": "Tutorial", + "Terms of service": "Condizioni di servizio" }, "MakeCopyMenu": { "However, it appears to be already identical.": "Tuttavia, sembra essere già identico.", @@ -549,7 +550,9 @@ "Legacy": "Vecchia versione", "Personal Site": "Sito personale", "Team Site": "Sito del team", - "Grist Templates": "Template di Grist" + "Grist Templates": "Template di Grist", + "Billing Account": "Dati di fatturazione", + "Manage Team": "Gestisci il team" }, "ChartView": { "Create separate series for each value of the selected column.": "Creare serie separate per ciascun valore delle colonne selezionate.", @@ -726,7 +729,19 @@ "Currency": "Valuta", "Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}": "ID Documento da usare quando le Api REST chiedono {{docId}}. Vedi {{apiURL}}", "Python version used": "Versione di Python in uso", - "Try API calls from the browser": "Prova le chiamate Api nel browser" + "Try API calls from the browser": "Prova le chiamate Api nel browser", + "Stop timing...": "Ferma cronometro...", + "Timing is on": "Cronometro attivo", + "You can make changes to the document, then stop timing to see the results.": "Puoi fare modifiche al documento, quindi fermare il cronometro e vedere il risultato.", + "Cancel": "Annulla", + "Reload data engine?": "Ricarica il motore dati?", + "Force reload the document while timing formulas, and show the result.": "Ricarica il documenti cronometrando le formule, e mostra il risultato.", + "Formula timer": "Cronometro per le formule", + "Reload data engine": "Ricarica il motore dati", + "Start timing": "Avvia cronometro", + "Time reload": "Ricarica tempo", + "Only available to document editors": "Disponibile solo per gli editor del documento", + "Only available to document owners": "Disponibile solo per i proprietari del documento" }, "DocumentUsage": { "Data Size": "Dimensione dei dati", @@ -1106,7 +1121,9 @@ "Add conditional style": "Aggiungi stile condizionale", "Error in style rule": "Errore nella regola di stile", "Row Style": "Stile riga", - "Rule must return True or False": "La regola deve restituire Vero o Falso" + "Rule must return True or False": "La regola deve restituire Vero o Falso", + "Conditional Style": "Stile condizionale", + "IF...": "SE..." }, "CurrencyPicker": { "Invalid currency": "Valuta non valida" @@ -1466,7 +1483,22 @@ "Check now": "Controlla adesso", "Checking for updates...": "Controllo gli aggiornamenti...", "Grist releases are at ": "Le release di Grist sono a ", - "Sandbox settings for data engine": "Impostazione della sandbox per il motore dati" + "Sandbox settings for data engine": "Impostazione della sandbox per il motore dati", + "Current authentication method": "Metodo di autenticazione attuale", + "No fault detected.": "Nessun problema rilevato.", + "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "O, come fallback, puoi impostare: {{bootKey}} nell'ambiente, e visitare: {{url}}", + "Results": "Risultati", + "Self Checks": "Auto-diagnostica", + "You do not have access to the administrator panel.\nPlease log in as an administrator.": "Non hai accesso al pannello di amministrazione.\nAccedi come amministratore.", + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone.", + "Details": "Dettagli", + "Notes": "Note", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist firma i cookie della sessione con una chiave segreta. Impostare questa chiave con la variabile d'ambiente GRIST_SESSION_SECRET. Se questa non è definita, Grist usa un default non modificabile. Potremmo rimuovere questo avviso in futuro, perché gli ID di sessione generati a partire dalla versione 1.1.16 sono intrinsecamente sicuri dal punto di vista crittografico.", + "Administrator Panel Unavailable": "Pannello di amministrazione non disponibile", + "Authentication": "Autenticazione", + "Check failed.": "Controllo fallito.", + "Check succeeded.": "Controllo riuscito.", + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone." }, "WelcomeCoachingCall": { "Maybe Later": "Forse più tardi", @@ -1587,5 +1619,15 @@ "Chart": "Grafico", "Custom": "Personalizzato", "Form": "Modulo" + }, + "TimingPage": { + "Average Time (s)": "Media tempi (sec)", + "Total Time (s)": "Tempo totale (sec)", + "Loading timing data. Don't close this tab.": "Caricamento dei dati cronometrici. Non chiudere questa scheda.", + "Max Time (s)": "Tempo massimo (sec)", + "Number of Calls": "Numero di chiamate", + "Table ID": "ID Tabella", + "Column ID": "ID colonna", + "Formula timer": "Cronometro formule" } } From 994432e5dec168b6107fd037fc4e5b67a976c78f Mon Sep 17 00:00:00 2001 From: Roman Holinec <3ko@pixeon.sk> Date: Fri, 28 Jun 2024 10:38:18 +0000 Subject: [PATCH 28/32] Translated using Weblate (Slovak) Currently translated at 100.0% (1337 of 1337 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sk/ --- static/locales/sk.client.json | 1065 ++++++++++++++++++++++++++++++++- 1 file changed, 1061 insertions(+), 4 deletions(-) diff --git a/static/locales/sk.client.json b/static/locales/sk.client.json index b5b72d72..7bc059b3 100644 --- a/static/locales/sk.client.json +++ b/static/locales/sk.client.json @@ -67,9 +67,9 @@ "Accounts": "Účty", "Add Account": "Pridať účet", "Document Settings": "Nastavenie Dokumentu", - "Manage Team": "Spravovať tím", + "Manage Team": "Riadiť Tím", "Pricing": "Cenník", - "Profile Settings": "Nastavenie profilu", + "Profile Settings": "Nastavenie Profilu", "Sign Out": "Odhlásiť sa", "Sign in": "Prihlásiť sa", "Switch Accounts": "Prepnúť Účty", @@ -375,7 +375,7 @@ }, "FieldConfig": { "COLUMN BEHAVIOR": "SPRÁVANIE STĹPCA", - "COLUMN LABEL AND ID": "ŠTÍTOK A ID STĹPCA", + "COLUMN LABEL AND ID": "OZNAČENIE A ID STĹPCA", "Clear and make into formula": "Vyčistiť a vytvoriť vzorec", "DESCRIPTION": "POPIS", "Clear and reset": "Vymazať a resetovať", @@ -545,7 +545,29 @@ "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Buďte opatrní, originál obsahuje zmeny, ktoré nie sú v tomto dokumente. Tieto zmeny budú prepísané.", "However, it appears to be already identical.": "Zdá sa však, že je už identický.", "Include the structure without any of the data.": "Zahrňte štruktúru bez akýchkoľvek údajov.", - "Cancel": "Zrušiť" + "Cancel": "Zrušiť", + "You do not have write access to the selected workspace": "Nemáte prístup na zápis do vybratého pracovného priestoru", + "It will be overwritten, losing any content not in this document.": "Bude prepísaný, pričom sa stratí všetok obsah, ktorý nie je v tomto dokumente.", + "Name": "Meno", + "You do not have write access to this site": "Nemáte prístup k zápisu na túto stránku", + "No destination workspace": "Žiadny cieľový pracovný priestor", + "Organization": "Organizácia", + "Overwrite": "Prepísať", + "Sign up": "Prihlásiť sa", + "Download full document and history": "Stiahnite si celý dokument a jeho históriu", + "Remove all data but keep the structure to use as a template": "Odstráňte všetky údaje, ale ponechajte štruktúru na použitie ako šablónu", + "Remove document history (can significantly reduce file size)": "Odstrániť históriu dokumentov (môže výrazne znížiť veľkosť súboru)", + "Download": "Stiahnuť", + "Download document": "Stiahnuť dokument", + "Original Has Modifications": "Originál Má Úpravy", + "Original Looks Unrelated": "Pôvodný Pohľad Nesúvisí", + "Original Looks Identical": "Pôvodný Pohľad Identický", + "Replacing the original requires editing rights on the original document.": "Nahradenie originálu vyžaduje práva na úpravu pôvodného dokumentu.", + "The original version of this document will be updated.": "Pôvodná verzia tohto dokumentu bude aktualizovaná.", + "To save your changes, please sign up, then reload this page.": "Ak chcete uložiť zmeny, zaregistrujte sa a potom znova načítajte túto stránku.", + "Update": "Aktualizovať", + "Update Original": "Aktualizovať Originál", + "Workspace": "Pracovný priestor" }, "GristDoc": { "Added new linked section to view {{viewName}}": "Pridaná nová prepojená sekcia na zobrazenie {{viewName}}", @@ -572,5 +594,1040 @@ }, "LeftPanelCommon": { "Help Center": "Centrum Pomoci" + }, + "NotifyUI": { + "Go to your free personal site": "Prejdite na svoju bezplatnú osobnú stránku", + "No notifications": "Žiadne upozornenia", + "Notifications": "Upozornenia", + "Manage billing": "Spravovať fakturáciu", + "Ask for help": "Požiadať o pomoc", + "Cannot find personal site, sorry!": "Nie je možné nájsť osobnú stránku, prepáčte!", + "Give feedback": "Dať spätnú väzbu", + "Renew": "Obnoviť", + "Report a problem": "Nahlásiť problém", + "Upgrade Plan": "Plán Inovácie" + }, + "OpenVideoTour": { + "YouTube video player": "Prehrávač videa YouTube", + "Grist Video Tour": "Grist Video Prehliadka", + "Video Tour": "Video Prehliadka" + }, + "PageWidgetPicker": { + "Add to Page": "Pridať na Stránku", + "Group by": "Zoskupiť podľa", + "Building {{- label}} widget": "Vytvoriť miniaplikáciu {{- label}}", + "Select Data": "Vybrať Údaje", + "Select Widget": "Vybrať Miniaplikáciu" + }, + "RecordLayout": { + "Updating record layout.": "Aktualizuje sa schéma záznamov." + }, + "Pages": { + "The following tables will no longer be visible_one": "Nasledujúca tabuľka už nebude zobraziteľná", + "The following tables will no longer be visible_other": "Nasledujúce tabuľky už nebudú zobraziteľné", + "Delete": "Odstrániť", + "Delete data and this page.": "Odstrániť údaje a túto stránku." + }, + "PermissionsWidget": { + "Deny All": "Odmietnuť Všetko", + "Read Only": "Iba na čítanie", + "Allow All": "Povoliť Všetko" + }, + "PluginScreen": { + "Import failed: ": "Import zlyhal: " + }, + "RecordLayoutEditor": { + "Save Layout": "Uložiť Rozloženie", + "Cancel": "Zrušiť", + "Add Field": "Pridať pole", + "Create New Field": "Vytvoriť nové pole", + "Show field {{- label}}": "Zobraziť pole {{- label}}" + }, + "RefSelect": { + "Add Column": "Pridať stĺpec", + "No columns to add": "Žiadne stĺpce na pridanie" + }, + "RightPanel": { + "CHART TYPE": "TYP GRAF", + "Data": "Údaje", + "Fields_one": "Pole", + "Fields_other": "Polia", + "Sort & Filter": "Triediť a Filtrovať", + "TRANSFORM": "TRANSFORMÁCIA", + "Theme": "Téma", + "WIDGET TITLE": "NÁZOV MINIAPLIKÁCIE", + "Enter redirect URL": "Zadať URL presmerovania", + "No field selected": "Nie je vybraté žiadne pole", + "COLUMN TYPE": "TYP STĹPEC", + "CUSTOM": "VOLITELNÝ", + "Change Widget": "Zmeniť Miniaplikáciu", + "Columns_one": "Stĺpec", + "Columns_other": "Stĺpce", + "DATA TABLE": "TABUĽKA ÚDAJOV", + "DATA TABLE NAME": "NÁZOV TABUĽKY ÚDAJOV", + "Detach": "Oddeliť", + "Edit Data Selection": "Upraviť Výber Údajov", + "GROUPED BY": "ZOSKUPIŤ PODĽA", + "ROW STYLE": "ŠTÝL RIADKU", + "Row Style": "Štýl Riadku", + "SELECT BY": "VYBRAŤ PODĽA", + "SELECTOR FOR": "VOLIČ PRE", + "SOURCE DATA": "ZDROJOVÉ ÚDAJE", + "Save": "Uložiť", + "Select Widget": "Vybrať Miniaplikáciu", + "Series_one": "Séria", + "Series_other": "Série", + "Widget": "Miniaplikácia", + "You do not have edit access to this document": "Nemáte prístup k úpravám tohto dokumentu", + "Add referenced columns": "Pridať referenčné stĺpce", + "Reset form": "Obnoviť formulár", + "Configuration": "Konfigurácia", + "Default field value": "Predvolená hodnota poľa", + "Display button": "Tlačidlo Zobrazenia", + "Enter text": "Zadať text", + "Required field": "Vyžadované pole", + "Field rules": "Pravidlá poľa", + "Field title": "Názov Poľa", + "Hidden field": "Skryté pole", + "Layout": "Rozloženie", + "Redirect automatically after submission": "Po odoslaní automaticky presmerovať", + "Redirection": "Presmerovať", + "Submission": "Návrh", + "Submit another response": "Odoslať inú odpoveď", + "Success text": "Text Dokončenia", + "Table column name": "Názov stĺpca tabuľky", + "Select a field in the form widget to configure.": "Vyberte pole v miniaplikácii formulára, ktoré chcete nakonfigurovať.", + "Submit button label": "Označenie tlačidla Odoslať" + }, + "RowContextMenu": { + "Copy anchor link": "Kopírovať odkaz na kotvu", + "Insert row": "Vložiť riadok", + "Insert row above": "Vložiť riadok vyššie", + "Duplicate rows_other": "Duplikovať riadky", + "Insert row below": "Vložiť riadok nižšie", + "Delete": "Odstrániť", + "Duplicate rows_one": "Duplikovať riadok", + "View as card": "Zobraziť ako kartu", + "Use as table headers": "Použiť ako hlavičky tabuľky" + }, + "ShareMenu": { + "Edit without affecting the original": "Upraviť bez ovplyvnenia originálu", + "Duplicate Document": "Duplikovať Dokument", + "Back to Current": "Späť na Aktuálne", + "Compare to {{termToUse}}": "Porovnať s {{termToUse}}", + "Access Details": "Podrobnosti Prístupu", + "Current Version": "Aktuálna verzia", + "Download": "Stiahnuť", + "Export CSV": "Exportovať CSV", + "Export XLSX": "Exportovať XLSX", + "Send to Google Drive": "Odoslať na Disk Google", + "Show in folder": "Zobraziť v priečinku", + "Unsaved": "Neuložené", + "Work on a Copy": "Pracovať na kópii", + "Share": "Zdieľať", + "Download...": "Stiahnuť ...", + "Comma Separated Values (.csv)": "Hodnoty oddelené čiarkou (.csv)", + "DOO Separated Values (.dsv)": "DOO Separated Values (.dsv)", + "Export as...": "Exportovať ako...", + "Microsoft Excel (.xlsx)": "Microsoft Excel (.xlsx)", + "Tab Separated Values (.tsv)": "Hodnoty oddelené tabulátorom (.tsv)", + "Manage Users": "Spravovať Používateľov", + "Replace {{termToUse}}...": "Nahradiť {{termToUse}}…", + "Original": "Originál", + "Return to {{termToUse}}": "Späť na {{termToUse}}", + "Save Copy": "Uložiť kópiu", + "Save Document": "Uložiť Dokument" + }, + "Tools": { + "Delete document tour?": "Chcete odstrániť Prehliadku?", + "How-to Tutorial": "Návod Ako na to", + "Code View": "Zobrazenie Kódu", + "Delete": "Odstrániť", + "Document History": "História Dokumentu", + "Settings": "Nastavenie", + "API Console": "API Konzola", + "Tour of this Document": "Prehliadka tohto Dokumentu", + "Validate Data": "Overiť Údaje", + "Access Rules": "Pravidlá Prístupu", + "Raw Data": "Nespracované Údaje", + "Return to viewing as yourself": "Vráťiť sa ku svojmu zobrazeniu", + "TOOLS": "NÁSTROJE" + }, + "TopBar": { + "Manage Team": "Riadiť Tím" + }, + "TriggerFormulas": { + "Any field": "Akékoľvek pole", + "Apply to new records": "Použiť na nové záznamy", + "Apply on record changes": "Použiť na zmeny záznamu", + "OK": "OK", + "Current field ": "Aktuálne pole ", + "Apply on changes to:": "Použiť pri zmenách na:", + "Cancel": "Zrušiť", + "Close": "Zavrieť" + }, + "TypeTransformation": { + "Apply": "Použiť", + "Cancel": "Zrušiť", + "Preview": "Náhľad", + "Revise": "Revidovať", + "Update formula (Shift+Enter)": "Aktualizovať vzorec (Shift+Enter)" + }, + "UserManagerModel": { + "Editor": "Editor", + "In Full": "Plne", + "No Default Access": "Žiadny Predvolený Prístup", + "None": "Žiadne", + "Owner": "Vlastník", + "View & Edit": "Zobraziť a Upraviť", + "View Only": "Iba Zobraziť", + "Viewer": "Divák" + }, + "ViewConfigTab": { + "Plugin: ": "Plugin: ", + "Make On-Demand": "Urobiť Na Požiadanie (On-Demand)", + "Advanced settings": "Pokročilé nastavenia", + "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Veľké tabuľky môžu byť označené ako „on-demand“, aby sa predišlo ich načítaniu do dátového stroja.", + "Blocks": "Bloky", + "Compact": "Kompaktný", + "Edit Card Layout": "Upraviť Rozloženie Karty", + "Form": "Formulár", + "Section: ": "Sekcia: ", + "Unmark On-Demand": "Odobrať označenie On-Demand" + }, + "ViewLayoutMenu": { + "Widget options": "Možnosti miniaplikácie", + "Advanced Sort & Filter": "Pokročilé Triedenie a Filtrovanie", + "Copy anchor link": "Kopírovať odkaz na kotvu", + "Data selection": "Výber údajov", + "Delete record": "Odstrániť záznam", + "Delete widget": "Odstrániť miniaplikáciu", + "Download as CSV": "Stiahnuť ako CSV", + "Download as XLSX": "Stiahnuť ako XLSX", + "Edit Card Layout": "Upraviť Rozloženie Karty", + "Open configuration": "Otvorte konfiguráciu", + "Print widget": "Tlačiť miniaplikáciu", + "Show raw data": "Zobraziť nespracované údaje", + "Collapse widget": "Zbaliť miniaplikáciu", + "Create a form": "Vytvoriť formulár", + "Add to page": "Pridať na stránku" + }, + "ViewSectionMenu": { + "SORT": "TRIEDIŤ", + "(customized)": "(prispôsobené)", + "(empty)": "(prázdne)", + "(modified)": "(zmenené)", + "Custom options": "Vlastné možnosti", + "FILTER": "FILTER", + "Revert": "Návrat", + "Save": "Uložiť", + "Update Sort&Filter settings": "Aktualizovať nastavenie triedenia a filtrovania" + }, + "WelcomeQuestions": { + "Education": "Vzdelávanie", + "Finance & Accounting": "Financie a Účtovníctvo", + "HR & Management": "HR a Manažment", + "Marketing": "Marketing", + "Media Production": "Mediálna Produkcia", + "Other": "Ostatné", + "Product Development": "Vývoj Produktov", + "Research": "Výskum", + "Sales": "Predaj", + "Type here": "Písať sem", + "Welcome to Grist!": "Vitajte v Grist!", + "IT & Technology": "IT a Technológie", + "What brings you to Grist? Please help us serve you better.": "Čo vás privádza do Gristu? Pomôžte nám, aby sme Vám lepšie poslúžili." + }, + "WidgetTitle": { + "Save": "Uložiť", + "Cancel": "Zrušiť", + "DATA TABLE NAME": "NÁZOV TABUĽKY ÚDAJOV", + "Override widget title": "Prepísať názov miniaplikácie", + "Provide a table name": "Zadajte názov tabuľky", + "WIDGET TITLE": "NÁZOV MINIAPLIKÁCIE", + "WIDGET DESCRIPTION": "POPIS MINIAPLIKÁCIE" + }, + "duplicatePage": { + "Note that this does not copy data, but creates another view of the same data.": "Upozornenie, že sa nekopírujú údaje, ale sa vytvorí iné zobrazenie rovnakých údajov.", + "Duplicate page {{pageName}}": "Duplikovať stránku {{pageName}}" + }, + "errorPages": { + "The requested page could not be found.{{separator}}Please check the URL and try again.": "Požadovanú stránku sa nepodarilo nájsť.{{separator}}Skontrolujte adresu URL a skúste to znova.", + "Sign up": "Prihlásiť sa", + "Powered by": "Poháňaný", + "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Ste prihlásený/-á ako {{email}}. Môžete sa prihlásiť pomocou iného účtu alebo požiadať správcu o prístup.", + "Form not found": "Formulár sa nenašiel", + "Access denied{{suffix}}": "Prístup odmietnutý {{suffix}}", + "Add account": "Pridať účet", + "Contact support": "Kontaktovať podporu", + "Error{{suffix}}": "Chyba {{suffix}}", + "Go to main page": "Prejdite na hlavnú stránku", + "Page not found{{suffix}}": "Stránka sa nenašla {{suffix}}", + "Sign in": "Prihlásiť sa", + "Sign in again": "Znovu sa prihlásiť", + "Sign in to access this organization's documents.": "Prihláste sa, ak chcete získať prístup k dokumentom tejto organizácie.", + "Signed out{{suffix}}": "Odhlásený {{suffix}}", + "Something went wrong": "Niečo sa pokazilo", + "There was an error: {{message}}": "Vyskytla sa chyba: {{message}}", + "There was an unknown error.": "Vyskytla sa neznáma chyba.", + "You are now signed out.": "Teraz ste odhlásení.", + "You do not have access to this organization's documents.": "Nemáte prístup k dokumentom tejto organizácie.", + "Account deleted{{suffix}}": "Účet odstránený {{suffix}}", + "Your account has been deleted.": "Váš účet bol odstránený.", + "An unknown error occurred.": "Vyskytla sa neznáma chyba.", + "Build your own form": "Vytvorte si vlastný formulár" + }, + "menus": { + "Select fields": "Vybrať polia", + "Any": "Akýkoľvek", + "Numeric": "Desatinné číslo", + "* Workspaces are available on team plans. ": "* Pracovné priestory sú k dispozícii v tímových plánoch. ", + "Upgrade now": "Vylepšiť teraz", + "Text": "Text", + "Integer": "Celé číslo", + "Toggle": "Prepínač", + "Date": "Dátum", + "DateTime": "Dátum a Čas", + "Choice": "Voľba", + "Choice List": "Zoznam Volieb", + "Reference": "Referencia", + "Attachment": "Príloha", + "Reference List": "Zoznam Referencií", + "Search columns": "Hľadať stĺpce" + }, + "modals": { + "Save": "Uložiť", + "Delete": "Odstrániť", + "Dismiss": "Odmietnuť", + "Undo to restore": "Späť na obnovenie", + "Cancel": "Zrušiť", + "Got it": "Mám to", + "Ok": "OK", + "Don't show again": "Už nezobrazovať", + "TIP": "TIP", + "Are you sure you want to delete these records?": "Naozaj chcete odstrániť tieto záznamy?", + "Are you sure you want to delete this record?": "Naozaj chcete odstrániť tento záznam?", + "Don't ask again.": "Už sa nepýtať.", + "Don't show again.": "Už nezobrazovať.", + "Don't show tips": "Nezobrazovať tipy" + }, + "NTextBox": { + "Lines": "Linie", + "false": "nepravda", + "true": "pravda", + "Field Format": "Formát Poľa", + "Multi line": "Viac riadok", + "Single line": "Jeden riadok" + }, + "TypeTransform": { + "Preview": "Náhľad", + "Revise": "Revidovať", + "Update formula (Shift+Enter)": "Aktualizovať vzorec (Shift+Enter)", + "Apply": "Použiť", + "Cancel": "Zrušiť" + }, + "ColumnInfo": { + "COLUMN ID: ": "ID STĹPCA: ", + "Cancel": "Zrušiť", + "Save": "Uložiť", + "COLUMN DESCRIPTION": "POPIS STĹPCA", + "COLUMN LABEL": "OZNAČENIE STĹPCA" + }, + "ConditionalStyle": { + "Row Style": "Štýl Riadku", + "Rule must return True or False": "Pravidlo musí vrátiť hodnotu Pravda alebo Nepravda", + "Conditional Style": "Podmienený Štýl", + "Add another rule": "Pridať ďalšie pravidlo", + "Add conditional style": "Pridať podmienený štýl", + "Error in style rule": "Chyba v pravidle štýlu", + "IF...": "AK..." + }, + "DiscussionEditor": { + "Resolve": "Vyriešiť", + "Cancel": "Zrušiť", + "Comment": "Komentovať", + "Edit": "Upraviť", + "Marked as resolved": "Označené ako vyriešené", + "Only current page": "Iba aktuálna stránka", + "Only my threads": "Iba moje vlákna", + "Open": "Otvoriť", + "Remove": "Odobrať", + "Reply": "Odpovedať", + "Reply to a comment": "Odpovedať na komentár", + "Save": "Uložiť", + "Show resolved comments": "Zobraziť vyriešené komentáre", + "Showing last {{nb}} comments": "Zobrazujú sa posledné {{nb}} komentáre", + "Started discussion": "Začať diskusiu", + "Write a comment": "Napísať komentár" + }, + "FieldBuilder": { + "Changing column type": "Zmena typu stĺpca", + "DATA FROM TABLE": "ÚDAJE Z TABUĽKY", + "Mixed format": "Zmiešaný formát", + "Mixed types": "Zmiešané typy", + "Revert field settings for {{colId}} to common": "Vrátiť nastavenie poľa pre {{colId}} na bežné", + "Save field settings for {{colId}} as common": "Uložiť nastavenie poľa pre {{colId}} ako bežné", + "Use separate field settings for {{colId}}": "Použiť samostatné nastavenie poľa pre {{colId}}", + "Apply Formula to Data": "Použiť Vzorec na Údaje", + "CELL FORMAT": "FORMÁT BUNIEK", + "Changing multiple column types": "Zmena viacerých typov stĺpcov" + }, + "FieldEditor": { + "Unable to finish saving edited cell": "Ukladanie upravenej bunky nie je možné dokončiť", + "It should be impossible to save a plain data value into a formula column": "Malo by byť nemožné uložiť obyčajnú hodnotu údajov do stĺpca vzorca" + }, + "NumericTextBox": { + "Currency": "Mena", + "Decimals": "Desatinné čísla", + "Number Format": "Formát Čísla", + "Default currency ({{defaultCurrency}})": "Predvolená mena ({{defaultCurrency}})", + "Field Format": "Formát Poľa", + "Spinner": "Spinner", + "Text": "Text", + "max": "max", + "min": "min" + }, + "WelcomeTour": { + "Add New": "Pridať Nové", + "Configuring your document": "Konfigurácia dokumentu", + "Editing Data": "Úprava Údajov", + "Reference": "Referencia", + "Share": "Zdieľať", + "Start with {{equal}} to enter a formula.": "Ak chcete zadať vzorec, začnite s {{equal}}.", + "Toggle the {{creatorPanel}} to format columns, ": "Prepnite {{creatorPanel}} na formátovanie stĺpcov, ", + "Browse our {{templateLibrary}} to discover what's possible and get inspired.": "Prezrite si našu {{templateLibrary}}, zistite, čo je možné a nechajte sa inšpirovať.", + "Enter": "Enter", + "Flying higher": "Letieť vyššie", + "Help Center": "Centrum pomoci", + "Make it relational! Use the {{ref}} type to link tables. ": "Urobte z toho vzťah! Na prepojenie tabuliek použite typ {{ref}}. ", + "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ": "Nastavte možnosti formátovania, vzorce alebo typy stĺpcov, ako sú dátumy, voľby alebo prílohy. ", + "Sharing": "Zdieľanie", + "Use the Share button ({{share}}) to share the document or export data.": "Na zdieľanie dokumentu alebo export údajov použite tlačidlo Zdieľať ({{share}}).", + "Use {{addNew}} to add widgets, pages, or import more data. ": "Ak chcete pridať miniaplikácie, stránky alebo importovať ďalšie údaje, použite {{addNew}}. ", + "Use {{helpCenter}} for documentation or questions.": "V prípade otázok, alebo potreby dokumentácie použite {{helpCenter}}.", + "Welcome to Grist!": "Vitajte v Grist!", + "convert to card view, select data, and more.": "previesť na zobrazenie kariet, výber údajov a ďalšie.", + "creator panel": "panel tvorcu", + "template library": "knižnica šablón", + "Building up": "Vybudovanie", + "Customizing columns": "Prispôsobenie stĺpcov", + "Double-click or hit {{enter}} on a cell to edit it. ": "Ak chcete bunku upraviť, dvakrát kliknite na bunku, alebo stlačte {{enter}}. " + }, + "Reference": { + "SHOW COLUMN": "ZOBRAZIŤ STĹPEC", + "CELL FORMAT": "FORMÁT BUNKY", + "Row ID": "ID Riadku" + }, + "GristTooltips": { + "Learn more.": "Naučiť sa viac.", + "Editing Card Layout": "Úprava Rozloženia Karty", + "Nested Filtering": "Vnorené Filtrovanie", + "Updates every 5 minutes.": "Aktualizácie každých 5 minút.", + "Use the \\u{1D6BA} icon to create summary (or pivot) tables, for totals or subtotals.": "Pomocou ikony \\u{1D6BA} vytvorte súhrnné (alebo kontingenčné) tabuľky pre súčty alebo medzisúčty.", + "Custom Widgets": "Vlastná Miniaplikácia", + "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Pomocou ikony 𝚺 vytvorte súhrnné (alebo kontingenčné) tabuľky pre súčty alebo medzisúčty.", + "Anchor Links": "Odkaz na Kotvu", + "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Neviete nájsť správne stĺpce? Kliknite na „Zmeniť miniaplikáciu“ a vyberte tabuľku s údajmi udalostí.", + "Example: {{example}}": "Príklad: {{example}}", + "Build simple forms right in Grist and share in a click with our new widget. {{learnMoreButton}}": "Vytvárajte jednoduché formuláre priamo v Grist a zdieľajte ich jediným kliknutím pomocou nášho nového widgetu. {{learnMoreButton}}", + "Clicking {{EyeHideIcon}} in each cell hides the field from this view without deleting it.": "Kliknutím na {{EyeHideIcon}} v každej bunke skryjete pole v tomto zobrazení bez toho, aby ste ho odstránili.", + "Link your new widget to an existing widget on this page.": "Prepojte svoju novú miniaplikáciu s existujúcou miniaplikáciou na tejto stránke.", + "Select the table containing the data to show.": "Vyberte tabuľku obsahujúcu údaje, ktoré chcete zobraziť.", + "Selecting Data": "Výber Údajov", + "The Raw Data page lists all data tables in your document, including summary tables and tables not included in page layouts.": "Na stránke Nespracované Údaje sú uvedené všetky tabuľky s údajmi vo vašom dokumente vrátane súhrnných tabuliek a tabuliek, ktoré nie sú zahrnuté v rozložení strán.", + "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Ak chcete vytvoriť kotviaci odkaz, ktorý používateľa zavedie do konkrétnej bunky, kliknite na riadok a stlačte {{shortcut}}.", + "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Môžete si vybrať z miniaplikácií, ktoré máte k dispozícii v rozbaľovacej ponuke, alebo môžete vložiť svoje vlastné poskytnutím ich úplnej adresy URL.", + "Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.": "Vzorce podporujú mnoho funkcií Excelu, plnú syntax Pythonu a zahŕňajú užitočného asistenta UI.", + "Apply conditional formatting to cells in this column when formula conditions are met.": "Použiť podmienené formátovanie na bunky v tomto stĺpci, keď sú splnené podmienky vzorca.", + "Apply conditional formatting to rows based on formulas.": "Použite podmienené formátovanie na riadky založené na vzorcoch.", + "Cells in a reference column always identify an {{entire}} record in that table, but you may select which column from that record to show.": "Bunky v referenčnom stĺpci vždy identifikujú {{entire}} záznam v tejto tabuľke, ale môžete si vybrať, ktorý stĺpec z tohto záznamu sa má zobraziť.", + "Click on “Open row styles” to apply conditional formatting to rows.": "Kliknutím na „Otvoriť štýly riadkov“ použijete na riadky podmienené formátovanie.", + "Click the Add New button to create new documents or workspaces, or import data.": "Kliknutím na tlačidlo Pridať nový vytvoríte nové dokumenty, pracovné priestory alebo importujete údaje.", + "Formulas that trigger in certain cases, and store the calculated value as data.": "Vzorce, ktoré sa v určitých prípadoch spúšťajú a ukladajú vypočítanú hodnotu ako údaje.", + "Only those rows will appear which match all of the filters.": "Zobrazia sa len tie riadky, ktoré zodpovedajú všetkým filtrom.", + "Pinned filters are displayed as buttons above the widget.": "Pripnuté filtre sa zobrazujú ako tlačidlá nad miniaplikáciou.", + "Pinning Filters": "Pripnúť Filtre", + "Raw Data page": "Stránka Nespracovaných Údajov", + "Rearrange the fields in your card by dragging and resizing cells.": "Usporiadajte polia na karte presunutím buniek a zmenou ich veľkosti.", + "Reference Columns": "Referenčné Stĺpce", + "Reference columns are the key to {{relational}} data in Grist.": "Referenčné stĺpce sú kľúčom k údajom {{relational}} v Grist.", + "Select the table to link to.": "Vyberte tabuľku, na ktorú chcete odkazovať.", + "They allow for one record to point (or refer) to another.": "Umožňujú, aby jeden záznam ukazoval (alebo odkazoval) na iný.", + "This is the secret to Grist's dynamic and productive layouts.": "Toto je tajomstvo dynamických a produktívnych rozložení Grist.", + "Try out changes in a copy, then decide whether to replace the original with your edits.": "Vyskúšajte zmeny v kópii a potom sa rozhodnite, či nahradíte originál vašimi úpravami.", + "Unpin to hide the the button while keeping the filter.": "Odopnutím skryjete tlačidlo pri zachovaní filtra.", + "Useful for storing the timestamp or author of a new record, data cleaning, and more.": "Užitočné na ukladanie časovej pečiatky alebo autora nového záznamu, čistenie dát a pod.", + "You can filter by more than one column.": "Môžete filtrovať podľa viac ako jedného stĺpca.", + "entire": "celý", + "relational": "vo vzťahu", + "Access Rules": "Pravidlá Prístupu", + "Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Pravidlá prístupu vám umožňujú vytvárať jemné pravidlá na určenie toho, kto môže vidieť alebo upravovať časti vášho dokumentu.", + "Add New": "Pridať Nové", + "You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Môžete si vybrať jednu z našich vopred pripravených miniaplikácií alebo vložiť svoj vlastnú poskytnutím jeho úplnej adresy URL.", + "Calendar": "Kalendár", + "To configure your calendar, select columns for start": { + "end dates and event titles. Note each column's type.": "Ak chcete nakonfigurovať svoj kalendár, vyberte stĺpce pre dátumy začiatku/ukončenia a názvy udalostí. Všimnite si typ každého stĺpca." + }, + "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID je náhodne vygenerovaný reťazec, ktorý je užitočný pre jedinečné identifikátory a kľúče odkazov.", + "Lookups return data from related tables.": "Vyhľadávania vracajú údaje zo súvisiacich tabuliek.", + "Use reference columns to relate data in different tables.": "Použite referenčné stĺpce na prepojenie údajov v rôznych tabuľkách.", + "These rules are applied after all column rules have been processed, if applicable.": "Tieto pravidlá sa použijú po spracovaní všetkých pravidiel stĺpcov, ak je to potrebné.", + "Filter displayed dropdown values with a condition.": "Filtrujte zobrazené rozbaľovacie hodnoty s podmienkou.", + "Forms are here!": "Formuláre sú tu!", + "Learn more": "Naučiť sa viac", + "Linking Widgets": "Prepojenie Miniaplikácií", + "The total size of all data in this document, excluding attachments.": "Celková veľkosť všetkých údajov v tomto dokumente okrem príloh." + }, + "ColumnTitle": { + "Add description": "Pridať popis", + "COLUMN ID: ": "ID STĹPCA: ", + "Column label": "Označenie stĺpca", + "Provide a column label": "Poskytnúť označenie stĺpca", + "Cancel": "Zrušiť", + "Column ID copied to clipboard": "ID stĺpca bolo skopírované do schránky", + "Column description": "Popis stĺpca", + "Save": "Uložiť", + "Close": "Zavrieť" + }, + "PagePanels": { + "Open Creator Panel": "Otvoriť Panel Tvorcu", + "Close Creator Panel": "Zatvoriť Panel Tvorcu" + }, + "FieldContextMenu": { + "Cut": "Vystrihnúť", + "Hide field": "Skryť pole", + "Paste": "Prilepiť", + "Clear field": "Vyčistiť pole", + "Copy": "Kopírovať", + "Copy anchor link": "Kopírovať odkaz na kotvu" + }, + "WebhookPage": { + "Clear Queue": "Vymazať Front", + "Webhook Settings": "Nastavenie Webhook", + "Removed webhook.": "Webhook bol odstránený.", + "Sorry, not all fields can be edited.": "Ľutujeme, nie všetky polia je možné upraviť.", + "Status": "Postavenie", + "Columns to check when update (separated by ;)": "Stĺpce na kontrolu pri aktualizácii (oddelené ;)", + "Cleared webhook queue.": "Vymazaný front webhook.", + "Event Types": "Typy Udalostí", + "Memo": "Memo", + "Name": "Meno", + "Enabled": "Povolené", + "Ready Column": "Pripravený Stĺpec", + "URL": "URL", + "Webhook Id": "Webhook ID", + "Table": "Tabuľka", + "Filter for changes in these columns (semicolon-separated ids)": "Filtrovať zmeny v týchto stĺpcoch (identifikátory oddelené bodkočiarkou)" + }, + "FormulaAssistant": { + "Capabilities": "Schopnosti", + "Community": "Spoločenstvo", + "Preview": "Náhľad", + "Need help? Our AI assistant can help.": "Potrebujete pomoc? Náš asistent UI vám môže pomôcť.", + "New Chat": "Nový Rozhovor", + "Tips": "Tipy", + "AI Assistant": "UI Asistent", + "Apply": "Použiť", + "For higher limits, {{upgradeNudge}}.": "Pre vyššie limity, {{upgradeNudge}}.", + "You have used all available credits.": "Využili ste všetky dostupné kredity.", + "You have {{numCredits}} remaining credits.": "Zostávajúce kredity máte na {{numCredits}}.", + "See our {{helpFunction}} and {{formulaCheat}}, or visit our {{community}} for more help.": "Pozrite si naše {{helpFunction}} a {{formulaCheat}}, alebo navštívte našu {{community}}, kde získate ďalšiu pomoc.", + "I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Pomôžem len vzorcom. Nemôžem vytvárať tabuľky, stĺpce a zobrazenia ani pravidlá prístupu na zápis.", + "Sign Up for Free": "Prihláste sa Zdarma", + "Sign up for a free Grist account to start using the Formula AI Assistant.": "Prihláste sa na bezplatný účet Grist a začnite používať UI asistenta Vzorcov.", + "Ask the bot.": "Opýtajte sa robota.", + "Data": "Údaje", + "Formula Help. ": "Pomocník Vzorca. ", + "Function List": "Zoznam Funkcií", + "Grist's AI Assistance": "Grist's UI Asistent", + "Grist's AI Formula Assistance. ": "Grist UI Asistent Vzorcov ", + "Formula Cheat Sheet": "Ťahák Vzorca", + "Regenerate": "Regenerovať", + "Save": "Uložiť", + "Cancel": "Zrušiť", + "Clear Conversation": "Vymazať Konverzáciu", + "Code View": "Zobrazenie Kódu", + "Hi, I'm the Grist Formula AI Assistant.": "Ahoj, som Grist UI asistent Vzorcov.", + "Learn more": "Naučiť sa viac", + "Press Enter to apply suggested formula.": "Stlačením klávesy Enter použijete navrhovaný vzorec.", + "There are some things you should know when working with me:": "Pri spolupráci so mnou by ste mali vedieť niekoľko vecí:", + "What do you need help with?": "S čím potrebujete pomôcť?", + "Formula AI Assistant is only available for logged in users.": "UI Asistent Vzorcov je k dispozícii len pre prihlásených užívateľov.", + "For higher limits, contact the site owner.": "Pre vyššie limity kontaktujte vlastníka stránky.", + "upgrade to the Pro Team plan": "povýsiť na plán Pro Team", + "upgrade your plan": "povýšiť svoj plán" + }, + "WelcomeSitePicker": { + "You have access to the following Grist sites.": "Máte prístup k nasledovným stránkam Grist.", + "Welcome back": "Vitajte späť", + "You can always switch sites using the account menu.": "Stránky môžete vždy prepínať pomocou ponuky konta." + }, + "DescriptionTextArea": { + "DESCRIPTION": "POPIS" + }, + "UserManager": { + "Guest": "Hosť", + "Grist support": "Podpora Grist", + "On": "Zapnuté", + "Open Access Rules": "Otvoriť Pravidlá Prístupu", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Keď odstránite svoj vlastný prístup, nebudete ho môcť získať späť bez pomoci niekoho iného s dostatočným prístupom k {{resourceType}}.", + "Allow anyone with the link to open.": "Povoliť otvorenie komukoľvek s odkazom.", + "Add {{member}} to your team": "Pridať {{member}} do svojho tímu", + "Anyone with link ": "Ktokoľvek s odkazom ", + "Cancel": "Zrušiť", + "Close": "Zavrieť", + "Collaborator": "Spolupracovník", + "Confirm": "Potvrdiť", + "Create a team to share with more people": "Vytvorte tím na zdieľanie s viacerými ľuďmi", + "Manage members of team site": "Spravovať členov tímovej lokality", + "Copy Link": "Kopírovať Link", + "Invite multiple": "Pozvať viacerých", + "Invite people to {{resourceType}}": "Pozvite ľudí do {{resourceType}}", + "Link copied to clipboard": "Odkaz bol skopírovaný do schránky", + "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Žiadny predvolený prístup neumožňuje udeliť prístup k jednotlivým dokumentom alebo pracovným priestorom, bez prístupu k celej tímovej lokalite.", + "Off": "Vypnuté", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Keď odstránite svoj vlastný prístup, nebudete ho môcť získať späť bez pomoci niekoho iného s dostatočným prístupom k {{name}}.", + "Outside collaborator": "Externý spolupracovník", + "Your role for this team site": "Vaša rola pre túto tímovú lokalitu", + "Your role for this {{resourceType}}": "Vaša rola pre tento {{resourceType}}", + "free collaborator": "slobodný spolupracovník", + "guest": "hosť", + "member": "člen", + "team site": "tímová stránka", + "{{collaborator}} limit exceeded": "{{collaborator}} prekročený limit", + "{{limitAt}} of {{limitTop}} {{collaborator}}s": "{{limitAt}} z {{limitTop}} {{collaborator}}", + "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Žiadny predvolený prístup neumožňuje udeliť prístup k jednotlivým dokumentom alebo pracovným priestorom, bez prístupu k celej tímovej lokalite.", + "User may not modify their own access.": "Používateľ nesmie meniť svoj vlastný prístup.", + "Public Access": "Verejný Prístup", + "Public access": "Verejný prístup", + "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Verejný prístup zdedený po {{parent}}. Ak ho chcete odstrániť, nastavte možnosť „Zdediť prístup“ na „Žiadny“.", + "Public access: ": "Verejný prístup: ", + "Remove my access": "Odstrániť môj prístup", + "Save & ": "Uložiť & ", + "Team member": "Člen tímu", + "User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "Používateľ zdedí povolenia od {{parent})}. Ak to chcete zmeniť, nastavte možnosť „Zdediť prístup“ na „Žiadny“.", + "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "Používateľ má prístup na zobrazenie zdroja {{resource}} v dôsledku manuálne nastaveného prístupu k vnútorným zdrojom. Ak sa odstráni, používateľ stratí prístup k vnútorným zdrojom.", + "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Používateľ zdedí povolenia od {{parent}}. Ak ich chcete odstrániť, nastavte možnosť „Zdediť prístup“ na „Žiadny“.", + "You are about to remove your own access to this {{resourceType}}": "Chystáte sa odstrániť svoj vlastný prístup k {{resourceType}}" + }, + "SupportGristNudge": { + "Admin Panel": "Panel Správcu", + "Thank you! Your trust and support is greatly appreciated. Opt out any time from the {{link}} in the user menu.": "Ďakujem! Vašu dôveru a podporu si veľmi vážime. Kedykoľvek sa môžete odhlásiť z {{link}} v používateľskej ponuke.", + "Opt in to Telemetry": "Prihláste sa do Telemetrie", + "Opted In": "Prihlásené", + "Support Grist": "Podpora Grist", + "Support Grist page": "Stránka Podpory Grist", + "Help Center": "Centrum Pomoci", + "Close": "Zavrieť", + "Contribute": "Prispieť" + }, + "SupportGristPage": { + "This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Táto inštancia je aktivovaná pre telemetriu. Toto môže zmeniť iba správca stránky.", + "Sponsor": "Sponzor", + "Help Center": "Centrum Pomoci", + "Manage Sponsorship": "Spravovať Sponzorovanie", + "Telemetry": "Telemetria", + "This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Táto inštancia je odhlásená z telemetrie. Toto môže zmeniť iba správca stránky.", + "GitHub": "GitHub", + "GitHub Sponsors page": "GitHub Stránka sponzorov", + "Home": "Domov", + "Opt in to Telemetry": "Prihláste sa do Telemetrie", + "Opt out of Telemetry": "Odhlásiť sa z Telemetrie", + "Sponsor Grist Labs on GitHub": "Sponzorujte Grist Labs na GitHub", + "Support Grist": "Podpora Grist", + "You can opt out of telemetry at any time from this page.": "Na tejto stránke sa môžete kedykoľvek odhlásiť z telemetrie.", + "We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Zhromažďujeme iba štatistiky používania, ako je podrobne uvedené v našom {{link}}, nikdy nedokumentujeme obsah.", + "You have opted in to telemetry. Thank you!": "Prihlásili ste sa do telemetrie. Ďakujeme!", + "You have opted out of telemetry.": "Deaktivovali ste telemetriu." + }, + "FormView": { + "Anyone with the link below can see the empty form and submit a response.": "Každý, kto má nižšie uvedený odkaz, môže vidieť prázdny formulár a odoslať odpoveď.", + "Are you sure you want to reset your form?": "Naozaj chcete resetovať svoj formulár?", + "View": "Zobrazenie", + "Copy code": "Kopírovať kód", + "Embed this form": "Vložiť tento formulár", + "Reset form": "Resetovať formulár", + "Preview": "Náhľad", + "Reset": "Resetovať", + "Save your document to publish this form.": "Ak chcete zverejniť tento formulár, uložte dokument.", + "Unpublish your form?": "Chcete zrušiť zverejnenie formuláru?", + "Share this form": "Zdieľať tento formulár", + "Publish": "Publikovať", + "Publish your form?": "Zverejniť svoj formulár?", + "Unpublish": "Nepublikovať", + "Code copied to clipboard": "Kód bol skopírovaný do schránky", + "Copy link": "Skopírovať odkaz", + "Link copied to clipboard": "Odkaz bol skopírovaný do schránky", + "Share": "Zdieľať" + }, + "AdminPanel": { + "Current version of Grist": "Aktuálna verzia Grist", + "Help us make Grist better": "Pomôžte nám zlepšiť Grist", + "Home": "Domov", + "Error checking for updates": "Kontrola chýb pri aktualizáciách", + "Grist is up to date": "Grist je aktuálny", + "Current authentication method": "Aktuálna metóda overovania", + "Details": "Podrobnosti", + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist umožňuje konfigurovať rôzne typy autentifikácie vrátane SAML a OIDC. Odporúčame povoliť jednu z týchto možností, ak je Grist prístupný cez sieť alebo je sprístupnený viacerým ľuďom.", + "No fault detected.": "Nebola zistená žiadna porucha.", + "Authentication": "Overenie", + "Check failed.": "Kontrola zlyhala.", + "Check succeeded.": "Kontrola sa podarila.", + "Notes": "Poznámky", + "Self Checks": "Sebakontroly", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist podpisuje súbory cookie relácie používateľa tajným kľúčom. Nastavte tento kľúč prostredníctvom premennej prostredia GRIST_SESSION_SECRET. Ak nie je nastavené, Grist sa vráti späť na pevne zakódované predvolené nastavenie. Toto upozornenie môžeme v budúcnosti odstrániť, pretože ID relácie generované od verzie 1.1.16 sú vo svojej podstate kryptograficky bezpečné.", + "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "Alebo môžete ako náhradnú možnosť nastaviť: {{bootKey}} vo voľbách prostredia a navštíviť ho: {{url}}", + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist umožňuje konfigurovať rôzne typy autentifikácie vrátane SAML a OIDC. Odporúčame povoliť jednu z týchto možností, ak je Grist prístupný cez sieť alebo je sprístupnený viacerým ľuďom.", + "Admin Panel": "Panel Správcu", + "Current": "Aktuálne", + "Sponsor": "Sponzor", + "Support Grist": "Podpora Grist", + "Support Grist Labs on GitHub": "Podpora Grist Labs na GitHub", + "Telemetry": "Telemetria", + "Version": "Verzia", + "Auto-check when this page loads": "Automatická kontrola pri načítaní tejto stránky", + "Check now": "Skontrolovať teraz", + "Checking for updates...": "Kontrola aktualizácií...", + "Error": "Chyba", + "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.": "Grist umožňuje veľmi výkonné vzorce pomocou Pythonu. Odporúčame nastaviť premennú prostredia GRIST_SANDBOX_FLAVOR na hodnotu gvisor, ak to váš hardvér podporuje (väčšina bude), aby sa vzorce v každom dokumente spúšťali v priestore izolovanom od ostatných dokumentov a izolovanom od siete.", + "Grist releases are at ": "Vydania Grist sú na adrese ", + "Last checked {{time}}": "Posledná kontrola {{time}}", + "Learn more.": "Naučiť sa viac.", + "No information available": "Nie sú dostupné žiadne informácie", + "Newer version available": "K dispozícii je nová verzia", + "OK": "OK", + "Administrator Panel Unavailable": "Panel Správcu je Nedostupný", + "Results": "Výsledky", + "You do not have access to the administrator panel.\nPlease log in as an administrator.": "Nemáte prístup k panelu spávcu.\nPrihláste sa ako správca.", + "Sandboxing": "Sandboxing", + "Sandbox settings for data engine": "Nastavenia sandboxu pre dátový stroj", + "Security Settings": "Bezpečnostné nastavenia", + "Updates": "Aktualizácie", + "unconfigured": "nekonfigurované", + "unknown": "neznáme" + }, + "TimingPage": { + "Table ID": "ID Tabuľky", + "Total Time (s)": "Celkový Čas (s)", + "Average Time (s)": "Priemerný Čas (s)", + "Column ID": "ID Stĺpca", + "Formula timer": "Časovač Vzorca", + "Number of Calls": "Počet Hovorov", + "Max Time (s)": "Maximálny Čas (s)", + "Loading timing data. Don't close this tab.": "Načítanie časových údajov. Túto kartu nezatvárajte." + }, + "ChoiceListEditor": { + "No choices to select": "Žiadne možnosti na výber", + "Error in dropdown condition": "Chyba v rozbaľovacej podmienke", + "No choices matching condition": "Žiadne možnosti zodpovedajúce podmienke" + }, + "DropdownConditionConfig": { + "Dropdown Condition": "Rozbaľovacia Podmienka", + "Set dropdown condition": "Nastaviť rozbaľovaciu podmienku", + "Invalid columns: {{colIds}}": "Neplatné stĺpce: {{colIds}}" + }, + "FormRenderer": { + "Select...": "Vybrať...", + "Submit": "Odoslať", + "Reset": "Resetovať", + "Search": "Hľadať" + }, + "widgetTypesMap": { + "Calendar": "Kalendár", + "Card": "Karta", + "Card List": "Zoznam Kariet", + "Chart": "Graf", + "Custom": "Vlastné", + "Form": "Formulár", + "Table": "Tabuľka" + }, + "ReferenceUtils": { + "No choices to select": "Žiadne možnosti na výber", + "Error in dropdown condition": "Chyba v rozbaľovacej podmienke", + "No choices matching condition": "Žiadne možnosti zodpovedajúce podmienke" + }, + "GridView": { + "Click to insert": "Kliknutím vložiť" + }, + "WelcomeCoachingCall": { + "On the call, we'll take the time to understand your needs and tailor the call to you. We can show you the Grist basics, or start working with your data right away to build the dashboards you need.": "Počas hovoru si nájdeme čas na pochopenie vašich potrieb a prispôsobíme sa vám. Môžeme vám ukázať základy Gristu alebo začať pracovať s vašimi údajmi hneď na zostavení panelov, ktoré potrebujete.", + "free coaching call": "bezplatný hovor koučovi", + "Maybe Later": "Možno Neskôr", + "Schedule Call": "Naplánovať Hovor", + "Schedule your {{freeCoachingCall}} with a member of our team.": "Naplánujte si {{freeCoachingCall}} s členom nášho tímu." + }, + "FormConfig": { + "Horizontal": "Horizontálne", + "Options Alignment": "Možnosti Zarovnania", + "Options Sort Order": "Možnosti Zoradiť Poradie", + "Field rules": "Pravidlá poľa", + "Required field": "Vyžadované pole", + "Ascending": "Vzostupne", + "Default": "Predvolené", + "Field Rules": "Pravidlá Poľa", + "Radio": "Rádio", + "Select": "Výber", + "Vertical": "Vertikálne", + "Field Format": "Formát Poľa", + "Descending": "Zostupne" + }, + "Editor": { + "Delete": "Odstrániť" + }, + "Menu": { + "Paragraph": "Odsek", + "Building blocks": "Stavebné bloky", + "Columns": "Stĺpce", + "Copy": "Kopírovať", + "Cut": "Vystrihnúť", + "Insert question above": "Vložte otázku vyššie", + "Insert question below": "Vložiť otázku nižšie", + "Paste": "Prilepiť", + "Separator": "Oddeľovač", + "Unmapped fields": "Nemapované polia", + "Header": "Hlavička" + }, + "UnmappedFieldsConfig": { + "Unmap fields": "Nemapovať polia", + "Map fields": "Mapovať polia", + "Mapped": "Zmapované", + "Select All": "Vybrať Všetko", + "Unmapped": "Nemapované", + "Clear": "Vyčistiť" + }, + "CustomView": { + "Some required columns aren't mapped": "Niektoré povinné stĺpce nie sú namapované", + "To use this widget, please map all non-optional columns from the creator panel on the right.": "Ak chcete použiť túto miniaplikáciu, namapujte všetky nepovinné stĺpce z panela tvorcov vpravo." + }, + "Field": { + "No values in show column of referenced table": "V zobrazenom stĺpci referenčnej tabuľky nie sú žiadne hodnoty", + "No choices configured": "Nie sú nakonfigurované žiadne voľby" + }, + "Toggle": { + "Field Format": "Formát Poľa", + "Checkbox": "Začiarkavacie políčko", + "Switch": "Prepínač" + }, + "ChoiceEditor": { + "No choices to select": "Žiadne možnosti na výber", + "Error in dropdown condition": "Chyba v rozbaľovacej podmienke", + "No choices matching condition": "Žiadne možnosti zodpovedajúce podmienke" + }, + "FormPage": { + "There was an error submitting your form. Please try again.": "Pri odosielaní formulára sa vyskytla chyba. Prosím skúste znova." + }, + "FormModel": { + "You don't have access to this form.": "K tomuto formuláru nemáte prístup.", + "Oops! The form you're looking for doesn't exist.": "Ojoj! Formulár, ktorý hľadáte, neexistuje.", + "Oops! This form is no longer published.": "Ojoj! Tento formulár už nie je zverejnený.", + "There was a problem loading the form.": "Pri načítavaní formulára sa vyskytol problém." + }, + "FormSuccessPage": { + "Form Submitted": "Formulár Odoslaný", + "Thank you! Your response has been recorded.": "Ďakujem! Vaša odpoveď bola zaznamenaná.", + "Submit new response": "Odoslať novú odpoveď" + }, + "DateRangeOptions": { + "This week": "Tento týždeň", + "Last 30 days": "Posledných 30 dní", + "Last Week": "Minulý Týždeň", + "Next 7 days": "Ďalších 7 dní", + "This month": "Tento mesiac", + "Last 7 days": "Posledných 7 dní", + "This year": "Tento rok", + "Today": "Dnes" + }, + "Section": { + "Insert section below": "Vložiť časť nižšie", + "Insert section above": "Vložiť časť vyššie" + }, + "DropdownConditionEditor": { + "Enter condition.": "Zadajte podmienku." + }, + "OnBoardingPopups": { + "Finish": "Dokončiť", + "Previous": "Predchádzajúce", + "Next": "Nasledujúce" + }, + "breadcrumbs": { + "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Môžete vykonať úpravy, ktoré vytvoria novú kópiu\na neovplyvnia pôvodný dokument.", + "unsaved": "neuložené", + "fiddle": "husle", + "override": "prepísať", + "recovery mode": "režim obnovenia", + "snapshot": "snímka" + }, + "pages": { + "Duplicate Page": "Duplikovať stránku", + "Remove": "Odobrať", + "Rename": "Premenovať", + "You do not have edit access to this document": "Nemáte prístup k úpravám tohto dokumentu" + }, + "SelectionSummary": { + "Copied to clipboard": "Skopírované do schránky" + }, + "SiteSwitcher": { + "Create new team site": "Vytvoriť novú tímovú lokalitu", + "Switch Sites": "Prepnúť lokality" + }, + "SortConfig": { + "Add Column": "Pridať Stĺpec", + "Empty values last": "Prázdne hodnoty zostávajú", + "Natural sort": "Prirodzené triedenie", + "Update Data": "Aktualizovať Údaje", + "Search Columns": "Hľadať stĺpce", + "Use choice position": "Použiť výber pozície" + }, + "SortFilterConfig": { + "Filter": "FILTER", + "Revert": "Návrat", + "Save": "Uložiť", + "Sort": "TRIEDIŤ", + "Update Sort & Filter settings": "Aktualizovať nastavenie triedenia a filtrovania" + }, + "ThemeConfig": { + "Appearance ": "Vzhľad ", + "Switch appearance automatically to match system": "Automaticky prepnúť vzhľad tak, aby zodpovedal systému" + }, + "ValidationPanel": { + "Rule {{length}}": "Pravidlo {{length}}", + "Update formula (Shift+Enter)": "Aktualizovať vzorec (Shift+Enter)" + }, + "VisibleFieldsConfig": { + "Cannot drop items into Hidden Fields": "Položky nie je možné umiestniť do Skrytých Polí", + "Clear": "Vyčistiť", + "Hidden Fields cannot be reordered": "Poradie Skrytých Polí nie je možné zmeniť", + "Select All": "Vybrať Všetko", + "Visible {{label}}": "Viditeľné {{label}}", + "Hide {{label}}": "Skryť {{label}}", + "Hidden {{label}}": "Skryté {{label}}", + "Show {{label}}": "Zobraziť {{label}}" + }, + "search": { + "No results": "Žiadne výsledky", + "Search in document": "Hľadať v dokumente", + "Search": "Hľadať", + "Find Next ": "Nájsť Ďaľší ", + "Find Previous ": "Nájsť Predošlý " + }, + "sendToDrive": { + "Sending file to Google Drive": "Odoslať súbor na Disk Google" + }, + "ACLUsers": { + "Example Users": "Príklady Používateľov", + "Users from table": "Používatelia z tabuľky", + "View As": "Zobraziť ako" + }, + "CellStyle": { + "CELL STYLE": "ŠTÝL BUNKY", + "Cell Style": "Štýl Bunky", + "Default cell style": "Predvolený štýl bunky", + "Mixed style": "Zmiešaný štýl", + "HEADER STYLE": "ŠTÝL HLAVYČKY", + "Open row styles": "Otvoriť štýly riadkov", + "Default header style": "Predvolený štýl hlavičky", + "Header Style": "Štýl Hlavičky" + }, + "ChoiceTextBox": { + "CHOICES": "VOĽBY" + }, + "ColumnEditor": { + "COLUMN DESCRIPTION": "POPIS STĹPCA", + "COLUMN LABEL": "OZNAČENIE STĹPCA" + }, + "CurrencyPicker": { + "Invalid currency": "Neplatná mena" + }, + "FormulaEditor": { + "Column or field is required": "Stĺpec alebo pole je vyžadované", + "Error in the cell": "Chyba v bunke", + "Errors in {{numErrors}} of {{numCells}} cells": "Chyby v {{numErrors}} z {{numCells}} buniek", + "Enter formula or {{button}}.": "Zadajte vzorec alebo {{button}}.", + "Expand Editor": "Rozbaliť Editor", + "use AI Assistant": "použite UI Asistenta", + "editingFormula is required": "Vyžaduje sa úprava Vzorca", + "Errors in all {{numErrors}} cells": "Chyby vo všetkých {{numErrors}} bunkách", + "Enter formula.": "Zadajte vzorec." + }, + "HyperLinkEditor": { + "[link label] url": "[link label] URL" + }, + "DescriptionConfig": { + "DESCRIPTION": "POPIS" + }, + "Clipboard": { + "Got it": "Mám to", + "Unavailable Command": "Nedostupný Príkaz" + }, + "buildViewSectionDom": { + "No data": "Žiadne údaje", + "No row selected in {{title}}": "V {{title}} nie je vybratý žiadny riadok", + "Not all data is shown": "Nezobrazujú sa všetky údaje" + }, + "FloatingEditor": { + "Collapse Editor": "Zbaliť Editor" + }, + "FloatingPopup": { + "Maximize": "Maximalizovať", + "Minimize": "Minimalizovať" + }, + "CardContextMenu": { + "Copy anchor link": "Kopírovať odkaz na kotvu", + "Delete card": "Odstrániť kartu", + "Duplicate card": "Duplikát karty", + "Insert card": "Vložiť kartu", + "Insert card above": "Vložiť kartu vyššie", + "Insert card below": "Vložiť kartu nižšie" + }, + "HiddenQuestionConfig": { + "Hidden fields": "Skryté polia" + }, + "FormContainer": { + "Build your own form": "Vytvorte si vlastný formulár", + "Powered by": "Poháňaný" + }, + "FormErrorPage": { + "Error": "Chyba" + }, + "MappedFieldsConfig": { + "Clear": "Vyčistiť", + "Map fields": "Mapovať polia", + "Mapped": "Zmapované", + "Select All": "Vybrať Všetko", + "Unmap fields": "Nemapovať polia", + "Unmapped": "Nemapované" + }, + "CreateTeamModal": { + "Cancel": "Zrušiť", + "Domain name is required": "Vyžaduje sa názov domény", + "Go to your site": "Prejdite na svoju stránku", + "Team name": "Názov tímu", + "Team name is required": "Vyžaduje sa názov tímu", + "Team site created": "Vytvorená tímová stránka", + "Team url": "Adresa url tímu", + "Work as a Team": "Pracujte ako Tím", + "Billing is not supported in grist-core": "Fakturácia nie je podporovaná v grist-core", + "Choose a name and url for your team site": "Vyberte si názov a adresu url pre svoju tímovú stránku", + "Create site": "Vytvoriť stránku", + "Domain name is invalid": "Názov domény je neplatný" + }, + "ViewAsBanner": { + "UnknownUser": "Neznámy Používateľ" + }, + "EditorTooltip": { + "Convert column to formula": "Previesť stĺpec na vzorec" + }, + "LanguageMenu": { + "Language": "Jazyk" + }, + "SearchModel": { + "Search all pages": "Prehľadať všetky stránky", + "Search all tables": "Prehľadať všetky tabuľky" + }, + "searchDropdown": { + "Search": "Vyhľadať" + }, + "Columns": { + "Remove Column": "Odobrať stĺpec" } } From 3769c5791551da4ec4eda906492455bc106ddf11 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Sat, 29 Jun 2024 19:53:47 +0000 Subject: [PATCH 29/32] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1340 of 1340 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index 06457ef5..74685c75 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -1617,7 +1617,11 @@ "Check failed.": "A verificação falhou.", "Current authentication method": "Método de autenticação atual", "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas.", - "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas." + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas.", + "Key to sign sessions with": "Chave para assinar sessões com", + "Session Secret": "Segredo da sessão", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "O Grist assina os cookies de sessão do usuário com uma chave secreta. Defina essa chave por meio da variável de ambiente GRIST_SESSION_SECRET. O Grist retorna a um padrão codificado quando ele não está definido. Poderemos remover esse aviso no futuro, pois os IDs de sessão gerados desde a versão 1.1.16 são inerentemente seguros em termos de criptografia.", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "O Grist assina os cookies de sessão do usuário com uma chave secreta. Defina essa chave por meio da variável de ambiente GRIST_SESSION_SECRET. O Grist retorna a um padrão codificado quando ele não está definido. Poderemos remover esse aviso no futuro, pois os IDs de sessão gerados desde a versão 1.1.16 são inerentemente seguros em termos de criptografia." }, "Field": { "No choices configured": "Nenhuma opção configurada", From d6d9d1c52e63aa04571c6d3eab4a7d7c50f05772 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Sat, 29 Jun 2024 19:54:59 +0000 Subject: [PATCH 30/32] Translated using Weblate (Spanish) Currently translated at 100.0% (1340 of 1340 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 9a54b025..b52afd76 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -1607,7 +1607,11 @@ "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "O, como alternativa, puedes configurar: {{bootKey}} en el entorno y visita: {{url}}", "You do not have access to the administrator panel.\nPlease log in as an administrator.": "No tienes acceso al panel de administrador.\nInicia sesión como administrador.", "Self Checks": "Controles automáticos", - "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist permite configurar diferentes tipos de autenticación, incluidos SAML y OIDC. Recomendamos habilitar uno de estos si se puede acceder a Grist a través de la red o si está disponible para varias personas." + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist permite configurar diferentes tipos de autenticación, incluidos SAML y OIDC. Recomendamos habilitar uno de estos si se puede acceder a Grist a través de la red o si está disponible para varias personas.", + "Session Secret": "Secreto de sesión", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist firma las cookies de sesión de usuario con una clave secreta. Establezca esta clave mediante la variable de entorno GRIST_SESSION_SECRET. Si no se establece, Grist vuelve a un valor predeterminado. Es posible que quitemos este aviso en el futuro, ya que los identificadores de sesión generados desde la versión 1.1.16 son intrínsecamente seguros desde el punto de vista criptográfico.", + "Key to sign sessions with": "Clave para firmar sesiones con", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist firma las cookies de sesión de usuario con una clave secreta. Establezca esta clave mediante la variable de entorno GRIST_SESSION_SECRET. Si no se establece, Grist vuelve a un valor predeterminado. Es posible que quitemos este aviso en el futuro, ya que los identificadores de sesión generados desde la versión 1.1.16 son intrínsecamente seguros desde el punto de vista criptográfico." }, "CreateTeamModal": { "Cancel": "Cancelar", From 0e777b1fcf3e9d68e2817b8b2247467cf34c724c Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Sat, 29 Jun 2024 19:55:29 +0000 Subject: [PATCH 31/32] Translated using Weblate (German) Currently translated at 100.0% (1340 of 1340 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index f7ed1c6d..8bb45bba 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -1613,7 +1613,11 @@ "No fault detected.": "Kein Fehler erkannt.", "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird.", "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "Als Ausweichmöglichkeit können Sie auch {{bootKey}} in der Umgebung einstellen und {{url}} besuchen", - "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird." + "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird.", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist signiert die Sitzungscookies der Benutzer mit einem geheimen Schlüssel. Bitte setzen Sie diesen Schlüssel über die Umgebungsvariable GRIST_SESSION_SECRET. Grist greift auf eine hart kodierte Voreinstellung zurück, wenn sie nicht gesetzt ist. Wir werden diesen Hinweis möglicherweise in Zukunft entfernen, da Sitzungs-IDs, die seit v1.1.16 erzeugt werden, von Natur aus kryptographisch sicher sind.", + "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist signiert die Sitzungscookies der Benutzer mit einem geheimen Schlüssel. Bitte setzen Sie diesen Schlüssel über die Umgebungsvariable GRIST_SESSION_SECRET. Grist greift auf eine hart kodierte Voreinstellung zurück, wenn sie nicht gesetzt ist. Wir werden diesen Hinweis möglicherweise in Zukunft entfernen, da Sitzungs-IDs, die seit v1.1.16 erzeugt werden, von Natur aus kryptographisch sicher sind.", + "Key to sign sessions with": "Schlüssel zum Anmelden von Sitzungen mit", + "Session Secret": "Sitzungsgeheimnis" }, "Section": { "Insert section above": "Abschnitt oben einfügen", From 61421e82510f924010e356ab6b36a8b7bad3afa7 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 1 Jul 2024 15:13:39 +0200 Subject: [PATCH 32/32] Create user last connection datetime (#935) Each time the a Grist page is reload the `last_connection_at` of the user is updated resolve [#924](https://github.com/gristlabs/grist-core/issues/924) --- app/gen-server/entity/User.ts | 3 ++ app/gen-server/lib/homedb/UsersManager.ts | 27 +++++++++++----- .../migration/1663851423064-UserUUID.ts | 17 +++++++--- .../migration/1664528376930-UserRefUnique.ts | 21 ++++++++---- .../1713186031023-UserLastConnection.ts | 18 +++++++++++ app/server/lib/requestUtils.ts | 4 +-- test/gen-server/migrations.ts | 32 ++++++++++++++++++- 7 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 app/gen-server/migration/1713186031023-UserLastConnection.ts diff --git a/app/gen-server/entity/User.ts b/app/gen-server/entity/User.ts index 2ed10169..c93837cb 100644 --- a/app/gen-server/entity/User.ts +++ b/app/gen-server/entity/User.ts @@ -29,6 +29,9 @@ export class User extends BaseEntity { @Column({name: 'first_login_at', type: Date, nullable: true}) public firstLoginAt: Date | null; + @Column({name: 'last_connection_at', type: Date, nullable: true}) + public lastConnectionAt: Date | null; + @OneToOne(type => Organization, organization => organization.owner) public personalOrg: Organization; diff --git a/app/gen-server/lib/homedb/UsersManager.ts b/app/gen-server/lib/homedb/UsersManager.ts index 8c0a5dca..168665f3 100644 --- a/app/gen-server/lib/homedb/UsersManager.ts +++ b/app/gen-server/lib/homedb/UsersManager.ts @@ -395,14 +395,6 @@ export class UsersManager { user.name = (profile && (profile.name || email.split('@')[0])) || ''; needUpdate = true; } - if (profile && !user.firstLoginAt) { - // set first login time to now (remove milliseconds for compatibility with other - // timestamps in db set by typeorm, and since second level precision is fine) - const nowish = new Date(); - nowish.setMilliseconds(0); - user.firstLoginAt = nowish; - needUpdate = true; - } if (!user.picture && profile && profile.picture) { // Set the user's profile picture if our provider knows it. user.picture = profile.picture; @@ -432,6 +424,25 @@ export class UsersManager { user.options = {...(user.options ?? {}), authSubject: userOptions.authSubject}; needUpdate = true; } + + // get date of now (remove milliseconds for compatibility with other + // timestamps in db set by typeorm, and since second level precision is fine) + const nowish = new Date(); + nowish.setMilliseconds(0); + if (profile && !user.firstLoginAt) { + // set first login time to now + user.firstLoginAt = nowish; + needUpdate = true; + } + const getTimestampStartOfDay = (date: Date) => { + const timestamp = Math.floor(date.getTime() / 1000); // unix timestamp seconds from epoc + const startOfDay = timestamp - (timestamp % 86400 /*24h*/); // start of a day in seconds since epoc + return startOfDay; + }; + if (!user.lastConnectionAt || getTimestampStartOfDay(user.lastConnectionAt) !== getTimestampStartOfDay(nowish)) { + user.lastConnectionAt = nowish; + needUpdate = true; + } if (needUpdate) { login.user = user; await manager.save([user, login]); diff --git a/app/gen-server/migration/1663851423064-UserUUID.ts b/app/gen-server/migration/1663851423064-UserUUID.ts index ba0e71b1..60c86668 100644 --- a/app/gen-server/migration/1663851423064-UserUUID.ts +++ b/app/gen-server/migration/1663851423064-UserUUID.ts @@ -1,5 +1,5 @@ -import {User} from 'app/gen-server/entity/User'; import {makeId} from 'app/server/lib/idUtils'; +import {chunk} from 'lodash'; import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; export class UserUUID1663851423064 implements MigrationInterface { @@ -16,11 +16,20 @@ export class UserUUID1663851423064 implements MigrationInterface { // Updating so many rows in a multiple queries is not ideal. We will send updates in chunks. // 300 seems to be a good number, for 24k rows we have 80 queries. const userList = await queryRunner.manager.createQueryBuilder() - .select("users") - .from(User, "users") + .select(["users.id", "users.ref"]) + .from("users", "users") .getMany(); userList.forEach(u => u.ref = makeId()); - await queryRunner.manager.save(userList, { chunk: 300 }); + + const userChunks = chunk(userList, 300); + for (const users of userChunks) { + await queryRunner.connection.transaction(async manager => { + const queries = users.map((user: any, _index: number, _array: any[]) => { + return queryRunner.manager.update("users", user.id, user); + }); + await Promise.all(queries); + }); + } // We are not making this column unique yet, because it can fail // if there are some old workers still running, and any new user diff --git a/app/gen-server/migration/1664528376930-UserRefUnique.ts b/app/gen-server/migration/1664528376930-UserRefUnique.ts index 27536042..149be01e 100644 --- a/app/gen-server/migration/1664528376930-UserRefUnique.ts +++ b/app/gen-server/migration/1664528376930-UserRefUnique.ts @@ -1,5 +1,5 @@ -import {User} from 'app/gen-server/entity/User'; import {makeId} from 'app/server/lib/idUtils'; +import {chunk} from 'lodash'; import {MigrationInterface, QueryRunner} from "typeorm"; export class UserRefUnique1664528376930 implements MigrationInterface { @@ -9,12 +9,21 @@ export class UserRefUnique1664528376930 implements MigrationInterface { // Update users that don't have unique ref set. const userList = await queryRunner.manager.createQueryBuilder() - .select("users") - .from(User, "users") - .where("ref is null") - .getMany(); + .select(["users.id", "users.ref"]) + .from("users", "users") + .where("users.ref is null") + .getMany(); userList.forEach(u => u.ref = makeId()); - await queryRunner.manager.save(userList, {chunk: 300}); + + const userChunks = chunk(userList, 300); + for (const users of userChunks) { + await queryRunner.connection.transaction(async manager => { + const queries = users.map((user: any, _index: number, _array: any[]) => { + return queryRunner.manager.update("users", user.id, user); + }); + await Promise.all(queries); + }); + } // Mark column as unique and non-nullable. const users = (await queryRunner.getTable('users'))!; diff --git a/app/gen-server/migration/1713186031023-UserLastConnection.ts b/app/gen-server/migration/1713186031023-UserLastConnection.ts new file mode 100644 index 00000000..52310a38 --- /dev/null +++ b/app/gen-server/migration/1713186031023-UserLastConnection.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner, TableColumn} from 'typeorm'; + +export class UserLastConnection1713186031023 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const sqlite = queryRunner.connection.driver.options.type === 'sqlite'; + const datetime = sqlite ? "datetime" : "timestamp with time zone"; + await queryRunner.addColumn('users', new TableColumn({ + name: 'last_connection_at', + type: datetime, + isNullable: true + })); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('users', 'last_connection_at'); + } +} diff --git a/app/server/lib/requestUtils.ts b/app/server/lib/requestUtils.ts index a6f29106..de0326d4 100644 --- a/app/server/lib/requestUtils.ts +++ b/app/server/lib/requestUtils.ts @@ -21,8 +21,8 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ? // Database fields that we permit in entities but don't want to cross the api. const INTERNAL_FIELDS = new Set([ - 'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId', - 'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin', + 'apiKey', 'billingAccountId', 'firstLoginAt', 'lastConnectionAt', 'filteredOut', 'ownerId', 'gracePeriodStart', + 'stripeCustomerId', 'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin', 'authSubject', 'usage', 'createdBy' ]); diff --git a/test/gen-server/migrations.ts b/test/gen-server/migrations.ts index 9b3e31e5..e6a45b98 100644 --- a/test/gen-server/migrations.ts +++ b/test/gen-server/migrations.ts @@ -42,6 +42,8 @@ import {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/mi import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit'; import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares'; import {Billing1711557445716 as BillingFeatures} from 'app/gen-server/migration/1711557445716-Billing'; +import {UserLastConnection1713186031023 + as UserLastConnection} from 'app/gen-server/migration/1713186031023-UserLastConnection'; const home: HomeDBManager = new HomeDBManager(); @@ -50,7 +52,8 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs, ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart, DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID, - Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures]; + Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures, + UserLastConnection]; // Assert that the "members" acl rule and group exist (or not). function assertMembersGroup(org: Organization, exists: boolean) { @@ -113,6 +116,33 @@ describe('migrations', function() { // be doing something. }); + it('can migrate UserUUID and UserUniqueRefUUID with user in table', async function() { + this.timeout(60000); + const runner = home.connection.createQueryRunner(); + + // Create 400 users to test the chunk (each chunk is 300 users) + const nbUsersToCreate = 400; + for (const migration of migrations) { + if (migration === UserUUID) { + for (let i = 0; i < nbUsersToCreate; i++) { + await runner.query(`INSERT INTO users (id, name, is_first_time_user) VALUES (${i}, 'name${i}', true)`); + } + } + + await (new migration()).up(runner); + } + + // Check that all refs are unique + const userList = await runner.manager.createQueryBuilder() + .select(["users.id", "users.ref"]) + .from("users", "users") + .getMany(); + const setOfUserRefs = new Set(userList.map(u => u.ref)); + assert.equal(nbUsersToCreate, userList.length); + assert.equal(setOfUserRefs.size, userList.length); + await addSeedData(home.connection); + }); + it('can correctly switch display_email column to non-null with data', async function() { this.timeout(60000); const sqlite = home.connection.driver.options.type === 'sqlite';