fix: Various fixes for collaborative editing beta (#2561)

* fix: Remove Saving… message when collab enabled

* chore: Add tracing extension to collaboration server

* fix: Incorrect debounce behavior due to missing timestamps on events, fixes abundence of notifications when editing in realtime collab mode

* fix: Reload document prompt when collab editing
This commit is contained in:
Tom Moor
2021-09-13 17:36:26 -07:00
committed by GitHub
parent a699dea286
commit 400e32da70
8 changed files with 92 additions and 20 deletions

View File

@ -12,17 +12,19 @@ import DocumentViews from "components/DocumentViews";
import Facepile from "components/Facepile"; import Facepile from "components/Facepile";
import NudeButton from "components/NudeButton"; import NudeButton from "components/NudeButton";
import Popover from "components/Popover"; import Popover from "components/Popover";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores"; import useStores from "hooks/useStores";
type Props = {| type Props = {|
document: Document, document: Document,
currentUserId: string,
|}; |};
function Collaborators(props: Props) { function Collaborators(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const user = useCurrentUser();
const currentUserId = user?.id;
const { users, presence } = useStores(); const { users, presence } = useStores();
const { document, currentUserId } = props; const { document } = props;
let documentPresence = presence.get(document.id); let documentPresence = presence.get(document.id);
documentPresence = documentPresence documentPresence = documentPresence

View File

@ -90,7 +90,7 @@ class DocumentScene extends React.Component<Props> {
this.updateIsDirty(); this.updateIsDirty();
} }
if (this.props.readOnly) { if (this.props.readOnly || auth.team?.collaborativeEditing) {
this.lastRevision = document.revision; this.lastRevision = document.revision;
if (document.title !== this.title) { if (document.title !== this.title) {

View File

@ -20,6 +20,7 @@ import Header from "components/Header";
import Tooltip from "components/Tooltip"; import Tooltip from "components/Tooltip";
import PublicBreadcrumb from "./PublicBreadcrumb"; import PublicBreadcrumb from "./PublicBreadcrumb";
import ShareButton from "./ShareButton"; import ShareButton from "./ShareButton";
import useCurrentTeam from "hooks/useCurrentTeam";
import useMobile from "hooks/useMobile"; import useMobile from "hooks/useMobile";
import useStores from "hooks/useStores"; import useStores from "hooks/useStores";
import DocumentMenu from "menus/DocumentMenu"; import DocumentMenu from "menus/DocumentMenu";
@ -67,7 +68,8 @@ function DocumentHeader({
headings, headings,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { auth, ui, policies } = useStores(); const team = useCurrentTeam();
const { ui, policies } = useStores();
const isMobile = useMobile(); const isMobile = useMobile();
const handleSave = React.useCallback(() => { const handleSave = React.useCallback(() => {
@ -81,7 +83,7 @@ function DocumentHeader({
const isNew = document.isNewDocument; const isNew = document.isNewDocument;
const isTemplate = document.isTemplate; const isTemplate = document.isTemplate;
const can = policies.abilities(document.id); const can = policies.abilities(document.id);
const canToggleEmbeds = auth.team && auth.team.documentEmbeds; const canToggleEmbeds = team?.documentEmbeds;
const canEdit = can.update && !isEditing; const canEdit = can.update && !isEditing;
const toc = ( const toc = (
@ -162,11 +164,10 @@ function DocumentHeader({
<TableOfContentsMenu headings={headings} /> <TableOfContentsMenu headings={headings} />
</TocWrapper> </TocWrapper>
)} )}
{!isPublishing && isSaving && <Status>{t("Saving")}</Status>} {!isPublishing && isSaving && !team.collaborativeEditing && (
<Collaborators <Status>{t("Saving")}</Status>
document={document} )}
currentUserId={auth.user ? auth.user.id : undefined} <Collaborators document={document} />
/>
{isEditing && !isTemplate && isNew && ( {isEditing && !isTemplate && isNew && (
<Action> <Action>
<TemplatesMenu <TemplatesMenu

View File

@ -0,0 +1,22 @@
// @flow
import debug from "debug";
const log = debug("server");
export default class Logger {
async onCreateDocument(data: { documentName: string }) {
log(`Created document "${data.documentName}"`);
}
async onConnect(data: { documentName: string }) {
log(`New connection to "${data.documentName}"`);
}
async onDisconnect(data: { documentName: string }) {
log(`Connection to "${data.documentName}" closed`);
}
async onUpgrade() {
log("Upgrading connection");
}
}

View File

@ -0,0 +1,40 @@
// @flow
import * as metrics from "../utils/metrics";
let count = 0;
export default class Tracing {
async onCreateDocument({ documentName }: { documentName: string }) {
metrics.increment("collaboration.create_document", { documentName });
// TODO: Waiting for `instance` available in payload
// metrics.gaugePerInstance(
// "collaboration.documents_count",
// instance.documents.size()
// );
}
async onAuthenticationFailed({ documentName }: { documentName: string }) {
metrics.increment("collaboration.authentication_failed", { documentName });
}
async onConnect({ documentName }: { documentName: string }) {
metrics.increment("collaboration.connect", { documentName });
metrics.gaugePerInstance("collaboration.connections_count", ++count);
}
async onDisconnect({ documentName }: { documentName: string }) {
metrics.increment("collaboration.disconnect", { documentName });
metrics.gaugePerInstance("collaboration.connections_count", --count);
// TODO: Waiting for `instance` available in payload
// metrics.gaugePerInstance(
// "collaboration.documents_count",
// instance.documents.size()
// );
}
async onChange({ documentName }: { documentName: string }) {
metrics.increment("collaboration.change", { documentName });
}
}

View File

@ -45,13 +45,21 @@ Event.beforeCreate((event) => {
}); });
Event.afterCreate((event) => { Event.afterCreate((event) => {
globalEventQueue.add(event, { removeOnComplete: true }); globalEventQueue.add(event);
}); });
// add can be used to send events into the event system without recording them // add can be used to send events into the event system without recording them
// in the database / audit trail // in the database or audit trail
Event.add = (event) => { Event.add = (event) => {
globalEventQueue.add(Event.build(event), { removeOnComplete: true }); const now = new Date();
globalEventQueue.add(
Event.build({
createdAt: now,
updatedAt: now,
...event,
})
);
}; };
Event.ACTIVITY_EVENTS = [ Event.ACTIVITY_EVENTS = [

View File

@ -1,12 +1,13 @@
// @flow // @flow
import http from "http"; import http from "http";
import { Logger } from "@hocuspocus/extension-logger";
import { Server } from "@hocuspocus/server"; import { Server } from "@hocuspocus/server";
import Koa from "koa"; import Koa from "koa";
import websocket from "koa-easy-ws"; import websocket from "koa-easy-ws";
import Router from "koa-router"; import Router from "koa-router";
import AuthenticationExtension from "../collaboration/authentication"; import AuthenticationExtension from "../collaboration/authentication";
import LoggerExtension from "../collaboration/logger";
import PersistenceExtension from "../collaboration/persistence"; import PersistenceExtension from "../collaboration/persistence";
import TracingExtension from "../collaboration/tracing";
export default function init(app: Koa, server: http.Server) { export default function init(app: Koa, server: http.Server) {
const router = new Router(); const router = new Router();
@ -15,7 +16,8 @@ export default function init(app: Koa, server: http.Server) {
extensions: [ extensions: [
new AuthenticationExtension(), new AuthenticationExtension(),
new PersistenceExtension(), new PersistenceExtension(),
new Logger(), new LoggerExtension(),
new TracingExtension(),
], ],
}); });

View File

@ -36,13 +36,10 @@ export default function init(app: Koa, server?: http.Server) {
// this queue processes global events and hands them off to services // this queue processes global events and hands them off to services
globalEventQueue.process(function (job) { globalEventQueue.process(function (job) {
Object.keys(eventProcessors).forEach((name) => { Object.keys(eventProcessors).forEach((name) => {
processorEventQueue.add( processorEventQueue.add({ ...job.data, service: name });
{ ...job.data, service: name },
{ removeOnComplete: true }
);
}); });
websocketsQueue.add(job.data, { removeOnComplete: true }); websocketsQueue.add(job.data);
}); });
processorEventQueue.process(function (job) { processorEventQueue.process(function (job) {