Add WebSocket support (#12)
This commit is contained in:
parent
3d27b7f313
commit
1aad0fce31
68 changed files with 2581 additions and 996 deletions
53
frontend/obsidian-plugin/src/views/history/history-view.scss
Normal file
53
frontend/obsidian-plugin/src/views/history/history-view.scss
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
.history-card {
|
||||
padding: var(--size-4-4);
|
||||
margin: var(--size-4-2);
|
||||
background-color: var(--color-base-00);
|
||||
border-radius: var(--radius-l);
|
||||
container-type: inline-size;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: rgba(var(--color-green-rgb), 0.2);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgba(var(--color-red-rgb), 0.2);
|
||||
}
|
||||
|
||||
.history-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--size-4-2);
|
||||
gap: var(--size-4-2);
|
||||
|
||||
@container (max-width: 300px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.history-card-title {
|
||||
font: var(--font-monospace);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-2);
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-card-timestamp {
|
||||
font-size: var(--font-ui-small);
|
||||
font-style: italic;
|
||||
color: var(--italic-color);
|
||||
}
|
||||
}
|
||||
|
||||
.history-card-message {
|
||||
font-size: var(--font-ui-medium);
|
||||
color: var(--color-base-70);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import "./history-view.scss";
|
||||
|
||||
import type { IconName, WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, setIcon } from "obsidian";
|
||||
|
||||
import { intlFormatDistance } from "date-fns";
|
||||
import type { HistoryEntry, SyncClient } from "sync-client";
|
||||
import { SyncType } from "sync-client";
|
||||
60
frontend/obsidian-plugin/src/views/logs/logs-view.scss
Normal file
60
frontend/obsidian-plugin/src/views/logs/logs-view.scss
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
.logs-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.verbosity-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: normal;
|
||||
gap: var(--size-4-2);
|
||||
margin: var(--size-4-4) var(--size-4-2);
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
max-width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.log-message {
|
||||
font: var(--font-monospace);
|
||||
margin-bottom: var(--size-2-1);
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
user-select: all;
|
||||
|
||||
.timestamp {
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--color-base-30);
|
||||
font-size: var(--font-ui-small);
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--bold-weight);
|
||||
margin-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
&.DEBUG {
|
||||
color: var(--color-base-50);
|
||||
}
|
||||
|
||||
&.INFO {
|
||||
color: var(--color-base-100);
|
||||
}
|
||||
|
||||
&.WARNING {
|
||||
color: rgb(var(--color-yellow-rgb));
|
||||
}
|
||||
|
||||
&.ERROR {
|
||||
color: rgb(var(--color-red-rgb));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import "./logs-view.scss";
|
||||
|
||||
import type { WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView } from "obsidian";
|
||||
import type { LogLine } from "sync-client";
|
||||
|
|
@ -7,8 +9,11 @@ export class LogsView extends ItemView {
|
|||
public static readonly TYPE = "logs-view";
|
||||
public static readonly ICON = "logs";
|
||||
|
||||
private static readonly MAX_OFFSET_FROM_BOTTOM_WITH_AUTO_SCROLL_PX = 300;
|
||||
|
||||
private logsContainer: HTMLElement | undefined;
|
||||
private readonly logLineToElement = new Map<LogLine, HTMLElement>();
|
||||
private minLogLevel: LogLevel = LogLevel.INFO;
|
||||
|
||||
public constructor(
|
||||
private readonly client: SyncClient,
|
||||
|
|
@ -56,10 +61,43 @@ export class LogsView extends ItemView {
|
|||
public async onOpen(): Promise<void> {
|
||||
const container = this.containerEl.children[1];
|
||||
container.addClass("logs-view");
|
||||
container.createEl("h4", { text: "VaultLink logs" });
|
||||
this.logsContainer = container.createDiv({ cls: "logs-container" });
|
||||
|
||||
this.updateView();
|
||||
const logLevels = [
|
||||
{ label: "Debug", value: LogLevel.DEBUG },
|
||||
{ label: "Info", value: LogLevel.INFO },
|
||||
{ label: "Warn", value: LogLevel.WARNING },
|
||||
{ label: "Error", value: LogLevel.ERROR }
|
||||
];
|
||||
|
||||
container.createDiv(
|
||||
{
|
||||
cls: "verbosity-selector"
|
||||
},
|
||||
(verbositySection) => {
|
||||
verbositySection.createEl("h4", {
|
||||
text: "VaultLink logs"
|
||||
});
|
||||
|
||||
verbositySection.createEl("select", {}, (dropdown) => {
|
||||
logLevels.forEach(({ label, value }) =>
|
||||
dropdown.createEl("option", { text: label, value })
|
||||
);
|
||||
|
||||
dropdown.value = this.minLogLevel;
|
||||
|
||||
dropdown.addEventListener("change", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
this.minLogLevel = dropdown.value as LogLevel;
|
||||
|
||||
this.logsContainer?.empty();
|
||||
this.logLineToElement.clear();
|
||||
this.updateView();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.logsContainer = container.createDiv({ cls: "logs-container" });
|
||||
}
|
||||
|
||||
private updateView(): void {
|
||||
|
|
@ -68,13 +106,20 @@ export class LogsView extends ItemView {
|
|||
return;
|
||||
}
|
||||
|
||||
const logs = this.client.logger.getMessages(LogLevel.DEBUG);
|
||||
const logs = this.client.logger.getMessages(this.minLogLevel);
|
||||
|
||||
if (this.logLineToElement.size === 0 && logs.length > 0) {
|
||||
// Clear the "No logs available yet" message
|
||||
container.empty();
|
||||
}
|
||||
|
||||
const shouldScroll =
|
||||
container.scrollTop == 0 ||
|
||||
container.scrollHeight -
|
||||
container.clientHeight -
|
||||
container.scrollTop <
|
||||
LogsView.MAX_OFFSET_FROM_BOTTOM_WITH_AUTO_SCROLL_PX;
|
||||
|
||||
logs.forEach((message) => {
|
||||
if (this.logLineToElement.has(message)) {
|
||||
return;
|
||||
|
|
@ -98,6 +143,8 @@ export class LogsView extends ItemView {
|
|||
container.createEl("p", {
|
||||
text: "No logs available yet."
|
||||
});
|
||||
} else if (shouldScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
@mixin number-card {
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--color-base-30);
|
||||
font-size: var(--font-ui-small);
|
||||
|
||||
&.good {
|
||||
background-color: rgba(var(--color-green-rgb), 0.35);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
background-color: rgba(var(--color-red-rgb), 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.vault-link-settings {
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--h2-size);
|
||||
|
||||
.version {
|
||||
@include number-card;
|
||||
margin: var(--size-2-2) 0 0 var(--size-4-2);
|
||||
background-color: var(--color-base-30);
|
||||
color: var(--color-base-70);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-ui-large);
|
||||
margin-top: var(--heading-spacing);
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="range"],
|
||||
.checkbox-container,
|
||||
.slider::-webkit-slider-thumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
textarea {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
height: 75px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import "./settings-tab.scss";
|
||||
|
||||
import type { App } from "obsidian";
|
||||
import { Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import type VaultLinkPlugin from "../vault-link-plugin";
|
||||
import type { StatusDescription } from "./status-description";
|
||||
import { LogsView } from "./logs-view";
|
||||
import { HistoryView } from "./history-view";
|
||||
import type VaultLinkPlugin from "src/vault-link-plugin";
|
||||
import type { SyncClient, SyncSettings } from "sync-client";
|
||||
import { HistoryView } from "../history/history-view";
|
||||
import { LogsView } from "../logs/logs-view";
|
||||
import type { StatusDescription } from "../status-description/status-description";
|
||||
|
||||
export class SyncSettingsTab extends PluginSettingTab {
|
||||
private editedServerUri: string;
|
||||
|
|
@ -220,7 +222,7 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
.addButton((button) =>
|
||||
button.setButtonText("Test connection").onClick(async () => {
|
||||
new Notice(
|
||||
(await this.syncClient.checkConnection()).message
|
||||
(await this.syncClient.checkConnection()).serverMessage
|
||||
);
|
||||
await this.statusDescription.updateConnectionState();
|
||||
})
|
||||
|
|
@ -246,29 +248,6 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
)
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Remote fetching frequency (seconds)")
|
||||
.setDesc(
|
||||
"Set how often should the plugin check for changes on the server. Lower values will increase the frequency of the checks making it easier to collaborate with others."
|
||||
)
|
||||
.setTooltip("todo, links to docs")
|
||||
.addSlider((text) =>
|
||||
text
|
||||
.setLimits(0.5, 60, 0.5)
|
||||
.setDynamicTooltip()
|
||||
.setInstant(false)
|
||||
.setValue(
|
||||
this.syncClient.getSettings()
|
||||
.fetchChangesUpdateIntervalMs / 1000
|
||||
)
|
||||
.onChange(async (value) =>
|
||||
this.syncClient.setSetting(
|
||||
"fetchChangesUpdateIntervalMs",
|
||||
value * 1000
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sync concurrency")
|
||||
.setDesc(
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.sync-status {
|
||||
display: flex;
|
||||
gap: var(--size-4-2);
|
||||
|
||||
* {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.initialize-button {
|
||||
padding: 0 var(--size-4-2);
|
||||
background: rgba(var(--color-red-rgb), 0.4);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import "./status-bar.scss";
|
||||
|
||||
import type { HistoryStats, SyncClient } from "sync-client";
|
||||
import type VaultLinkPlugin from "../vault-link-plugin";
|
||||
import type VaultLinkPlugin from "../../vault-link-plugin";
|
||||
|
||||
export class StatusBar {
|
||||
private readonly statusBarItem: HTMLElement;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
@mixin number-card {
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--color-base-30);
|
||||
font-size: var(--font-ui-small);
|
||||
|
||||
&.good {
|
||||
background-color: rgba(var(--color-green-rgb), 0.35);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
background-color: rgba(var(--color-red-rgb), 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.status-description {
|
||||
margin: var(--p-spacing) 0;
|
||||
|
||||
.number {
|
||||
@include number-card;
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--bold-weight);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: rgb(var(--color-red-rgb));
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: rgb(var(--color-yellow-rgb));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import "./status-description.scss";
|
||||
|
||||
import type {
|
||||
HistoryStats,
|
||||
CheckConnectionResult,
|
||||
NetworkConnectionStatus,
|
||||
SyncClient
|
||||
} from "sync-client";
|
||||
|
||||
export class StatusDescription {
|
||||
private lastHistoryStats: HistoryStats | undefined;
|
||||
private lastRemaining: number | undefined;
|
||||
private lastConnectionState: CheckConnectionResult | undefined;
|
||||
private lastConnectionState: NetworkConnectionStatus | undefined;
|
||||
|
||||
private statusChangeListeners: (() => void)[] = [];
|
||||
|
||||
|
|
@ -26,9 +28,13 @@ export class StatusDescription {
|
|||
}
|
||||
);
|
||||
|
||||
this.syncClient.addOnSettingsChangeListener(() => {
|
||||
void this.updateConnectionState();
|
||||
});
|
||||
this.syncClient.addWebSocketStatusChangeListener(
|
||||
() => void this.updateConnectionState()
|
||||
);
|
||||
|
||||
this.syncClient.addOnSettingsChangeListener(
|
||||
() => void this.updateConnectionState()
|
||||
);
|
||||
}
|
||||
|
||||
public async updateConnectionState(): Promise<void> {
|
||||
|
|
@ -59,7 +65,15 @@ export class StatusDescription {
|
|||
|
||||
if (!this.lastConnectionState.isSuccessful) {
|
||||
container.createSpan({
|
||||
text: `VaultLink failed to connect to the remote server with the error "${this.lastConnectionState.message}"`,
|
||||
text: `VaultLink failed to connect to the remote server with error '${this.lastConnectionState.serverMessage}'`,
|
||||
cls: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.lastConnectionState.isWebSocketConnected) {
|
||||
container.createSpan({
|
||||
text: `${this.lastConnectionState.serverMessage} but the WebSocket connection could not be established.`,
|
||||
cls: "error"
|
||||
});
|
||||
return;
|
||||
Loading…
Add table
Add a link
Reference in a new issue