(core) Add Support Grist page and nudge

Summary:
Adds a new Support Grist page (accessible only in grist-core), containing
options to opt in to telemetry and sponsor Grist Labs on GitHub.

A nudge is also shown in the doc menu, which can be collapsed or permanently
dismissed.

Test Plan: Browser and server tests.

Reviewers: paulfitz, dsagal

Reviewed By: paulfitz

Subscribers: jarek, dsagal

Differential Revision: https://phab.getgrist.com/D3926
This commit is contained in:
George Gevoian
2023-07-04 17:21:34 -04:00
parent 051c6d52fe
commit 35237a5835
47 changed files with 1743 additions and 365 deletions

View File

@@ -1,3 +1,7 @@
import {InstallPrefs} from "app/common/Install";
import {ApiError} from "app/common/ApiError";
import {InstallProperties, installPropertyKeys} from "app/common/InstallAPI";
import {nativeValues} from "app/gen-server/lib/values";
import {BaseEntity, Column, Entity, PrimaryColumn} from "typeorm";
@Entity({name: 'activations'})
@@ -9,9 +13,47 @@ export class Activation extends BaseEntity {
@Column({name: 'key', type: 'text', nullable: true})
public key: string|null;
@Column({type: nativeValues.jsonEntityType, nullable: true})
public prefs: InstallPrefs|null;
@Column({name: 'created_at', default: () => "CURRENT_TIMESTAMP"})
public createdAt: Date;
@Column({name: 'updated_at', default: () => "CURRENT_TIMESTAMP"})
public updatedAt: Date;
public checkProperties(props: any): props is Partial<InstallProperties> {
for (const key of Object.keys(props)) {
if (!installPropertyKeys.includes(key)) {
throw new ApiError(`Unrecognized property ${key}`, 400);
}
}
return true;
}
public updateFromProperties(props: Partial<InstallProperties>) {
if (props.prefs === undefined) { return; }
if (props.prefs === null) {
this.prefs = null;
} else {
this.prefs = this.prefs || {};
if (props.prefs.telemetry !== undefined) {
this.prefs.telemetry = this.prefs.telemetry || {};
if (props.prefs.telemetry.telemetryLevel !== undefined) {
this.prefs.telemetry.telemetryLevel = props.prefs.telemetry.telemetryLevel;
}
}
for (const key of Object.keys(this.prefs) as Array<keyof InstallPrefs>) {
if (this.prefs[key] === null) {
delete this.prefs[key];
}
}
if (Object.keys(this.prefs).length === 0) {
this.prefs = null;
}
}
}
}

View File

@@ -1,10 +1,8 @@
import {ApiError} from 'app/common/ApiError';
import {DocumentUsage} from 'app/common/DocUsage';
import {hashId} from 'app/common/hashingUtils';
import {Role} from 'app/common/roles';
import {DocumentOptions, DocumentProperties, documentPropertyKeys,
DocumentType, NEW_DOCUMENT_CODE, TutorialMetadata} from "app/common/UserAPI";
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {DocumentOptions, DocumentProperties, documentPropertyKeys, DocumentType,
NEW_DOCUMENT_CODE} from "app/common/UserAPI";
import {nativeValues} from 'app/gen-server/lib/values';
import {Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryColumn} from "typeorm";
import {AclRuleDoc} from "./AclRule";
@@ -92,7 +90,7 @@ export class Document extends Resource {
return super.checkProperties(props, documentPropertyKeys);
}
public updateFromProperties(props: Partial<DocumentProperties>, dbManager?: HomeDBManager) {
public updateFromProperties(props: Partial<DocumentProperties>) {
super.updateFromProperties(props);
if (props.isPinned !== undefined) { this.isPinned = props.isPinned; }
if (props.urlId !== undefined) {
@@ -135,9 +133,6 @@ export class Document extends Resource {
}
if (props.options.tutorial.lastSlideIndex !== undefined) {
this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex;
if (dbManager && this.options.tutorial.numSlides) {
this._emitTutorialProgressChangeEvent(dbManager, this.options.tutorial);
}
}
}
}
@@ -154,26 +149,6 @@ export class Document extends Resource {
}
}
}
private _emitTutorialProgressChangeEvent(
dbManager: HomeDBManager,
tutorialMetadata: TutorialMetadata
) {
const lastSlideIndex = tutorialMetadata.lastSlideIndex;
const numSlides = tutorialMetadata.numSlides;
const percentComplete = lastSlideIndex !== undefined && numSlides !== undefined
? Math.floor((lastSlideIndex / numSlides) * 100)
: undefined;
dbManager?.emit('tutorialProgressChanged', {
full: {
tutorialForkIdDigest: hashId(this.id),
tutorialTrunkIdDigest: this.trunkId ? hashId(this.trunkId) : undefined,
lastSlideIndex,
numSlides,
percentComplete,
},
});
}
}
// Check that icon points to an expected location. This will definitely

View File

@@ -20,6 +20,7 @@ export class Activations {
if (!activation) {
activation = manager.create(Activation);
activation.id = makeId();
activation.prefs = {};
await activation.save();
}
return activation;

View File

@@ -89,14 +89,6 @@ export const NotifierEvents = StringUnion(
export type NotifierEvent = typeof NotifierEvents.type;
export const HomeDBTelemetryEvents = StringUnion(
'tutorialProgressChanged',
);
export type HomeDBTelemetryEvent = typeof HomeDBTelemetryEvents.type;
export type Event = NotifierEvent | HomeDBTelemetryEvent;
// Nominal email address of a user who can view anything (for thumbnails).
export const PREVIEWER_EMAIL = 'thumbnail@getgrist.com';
@@ -324,7 +316,7 @@ export class HomeDBManager extends EventEmitter {
orgOnly: true
}];
public emit(event: Event, ...args: any[]): boolean {
public emit(event: NotifierEvent, ...args: any[]): boolean {
return super.emit(event, ...args);
}
@@ -1960,7 +1952,7 @@ export class HomeDBManager extends EventEmitter {
// Update the name and save.
const doc: Document = queryResult.data;
doc.checkProperties(props);
doc.updateFromProperties(props, this);
doc.updateFromProperties(props);
if (forkId) {
await manager.save(doc);
return {status: 200};

View File

@@ -0,0 +1,16 @@
import {nativeValues} from 'app/gen-server/lib/values';
import {MigrationInterface, QueryRunner, TableColumn} from 'typeorm';
export class ActivationPrefs1682636695021 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.addColumn('activations', new TableColumn({
name: 'prefs',
type: nativeValues.jsonType,
isNullable: true,
}));
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropColumn('activations', 'prefs');
}
}