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:
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
22
server/collaboration/logger.js
Normal file
22
server/collaboration/logger.js
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
40
server/collaboration/tracing.js
Normal file
40
server/collaboration/tracing.js
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@ -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 = [
|
||||||
|
@ -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(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
Reference in New Issue
Block a user