Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 32 additions & 33 deletions docker/example.env
Original file line number Diff line number Diff line change
@@ -1,45 +1,44 @@
#url of the frontend, this must be accessible by your clients browser
# Copy this file to `.env` before starting the containers

### === Required Config ===

# URL of the frontend (accessible by browser)
MONKEYTYPE_FRONTENDURL=http://myserver:8080

#url of the backend server, this must be accessible by your clients browser
# URL of the backend (accessible by browser)
MONKEYTYPE_BACKENDURL=http://myserver:5005

# firebase config
# uncomment below config if you need user accounts
#FIREBASE_APIKEY=
#FIREBASE_AUTHDOMAIN=
#FIREBASE_PROJECTID=
#FIREBASE_STORAGEBUCKET=
#FIREBASE_MESSAGINGSENDERID=
#FIREBASE_APPID=


# email server config
# uncomment below if you want to send emails for e.g. password reset
#EMAIL_HOST=mail.myserver
#EMAIL_USER=mailuser
#EMAIL_PASS=mailpass
#EMAIL_PORT=465
#EMAIL_FROM="Support <noreply@myserver>"

# google recaptcha
# uncomment below config if you need user accounts
# you can use these defaults if you host this privately
#RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
#RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
### === Optional: Google reCAPTCHA ===

# Default keys below work for localhost/private instances
# RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
# RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET=

# use alternative ports
### === Optional: Firebase ===

# port of the frontend http server
# HTTP_PORT=8080
# Uncomment if using user accounts
# FIREBASE_APIKEY=AIzaSy********
# FIREBASE_AUTHDOMAIN=your-app.firebaseapp.com
# FIREBASE_PROJECTID=your-app
# FIREBASE_STORAGEBUCKET=your-app.appspot.com
# FIREBASE_MESSAGINGSENDERID=1234567890
# FIREBASE_APPID=1:1234567890:web:abcdef123456

# port of the backend api server
# BACKEND_PORT=5005
### === Optional: Email Server ===
# Enables email (e.g. password reset)

# port of the redis server, not exposed by default
# REDIS_PORT=6379
# EMAIL_HOST=smtp.mailserver.com
# [email protected]
# EMAIL_PASS=password
# EMAIL_PORT=465
# EMAIL_FROM="Support <[email protected]>"

### === Optional: Custom Ports ===

# port of the mongodb server, not exposed by default
# HTTP_PORT=8080
# BACKEND_PORT=5005
# REDIS_PORT=6379
# MONGO_PORT=27017
21 changes: 0 additions & 21 deletions frontend/src/html/pages/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,6 @@
step="1"
value=""
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
<div class="buttons">
<button data-config-value="off">off</button>
Expand Down Expand Up @@ -288,9 +285,6 @@
step="1"
value=""
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
<div class="buttons">
<button data-config-value="off">off</button>
Expand Down Expand Up @@ -322,9 +316,6 @@
step="1"
value=""
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
<div class="buttons">
<button data-config-value="off">off</button>
Expand Down Expand Up @@ -792,9 +783,6 @@
step="1"
value=""
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
<div class="buttons">
<button data-config-value="off">off</button>
Expand Down Expand Up @@ -1020,9 +1008,6 @@
min="10"
max="90"
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -1131,9 +1116,6 @@
class="input"
min="0"
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
</div>
</div>
Expand All @@ -1154,9 +1136,6 @@
class="input"
tabindex="0"
/>
<button class="save no-auto-handle">
<i class="fas fa-save fa-fw"></i>
</button>
</div>
</div>
</div>
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/styles/settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
.inputAndButton {
display: grid;
grid-template-columns: auto min-content;
gap: 0.5rem;
// gap: 0.5rem;
margin-bottom: 0.5rem;

span {
Expand All @@ -104,6 +104,20 @@
margin-right: 0rem;
}
}

.hasError {
animation: shake 0.1s ease-in-out infinite;
}

.statusIndicator {
opacity: 0;
}
&:has(input:focus),
&:has([data-indicator-status="failed"]) {
.statusIndicator {
opacity: 1;
}
}
}

.rangeGroup {
Expand Down
101 changes: 33 additions & 68 deletions frontend/src/ts/commandline/commandline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Command, CommandsSubgroup, CommandWithValidation } from "./types";
import { areSortedArraysEqual } from "../utils/arrays";
import { parseIntOptional } from "../utils/numbers";
import { debounce } from "throttle-debounce";
import { createInputEventHandler } from "../elements/input-validation";

type CommandlineMode = "search" | "input";
type InputModeParams = {
Expand Down Expand Up @@ -618,6 +619,18 @@ async function runActiveCommand(): Promise<void> {
value: command.defaultValue?.() ?? "",
icon: command.icon ?? "fa-chevron-right",
};
if ("validation" in command && !handlersCache.has(command.id)) {
const commandWithValidation = command as CommandWithValidation<unknown>;
const handler = createInputEventHandler(
updateValidationResult,
commandWithValidation.validation,
"inputValueConvert" in commandWithValidation
? commandWithValidation.inputValueConvert
: undefined
);
handlersCache.set(command.id, handler);
}

await updateInput(inputModeParams.value as string);
hideCommands();
} else if (command.subgroup) {
Expand Down Expand Up @@ -788,48 +801,10 @@ function updateValidationResult(
}
}

async function isValid(
checkValue: unknown,
originalValue: string,
originalInput: HTMLInputElement,
validation: CommandWithValidation<unknown>["validation"]
): Promise<void> {
updateValidationResult({ status: "checking" });

if (validation.schema !== undefined) {
const schemaResult = validation.schema.safeParse(checkValue);

if (!schemaResult.success) {
updateValidationResult({
status: "failed",
errorMessage: schemaResult.error.errors
.map((err) => err.message)
.join(", "),
});
return;
}
}

if (validation.isValid === undefined) {
updateValidationResult({ status: "success" });
return;
}

const result = await validation.isValid(checkValue);
if (originalInput.value !== originalValue) {
//value has change in the meantime, discard result
return;
}

if (result === true) {
updateValidationResult({ status: "success" });
} else {
updateValidationResult({
status: "failed",
errorMessage: result,
});
}
}
/*
* Handlers needs to be created only once per command to ensure they debounce with the given delay
*/
const handlersCache = new Map<string, (e: Event) => Promise<void>>();

const modal = new AnimatedModal({
dialogId: "commandLine",
Expand Down Expand Up @@ -921,34 +896,24 @@ const modal = new AnimatedModal({
}
});

input.addEventListener(
"input",
debounce(100, async (e) => {
if (
inputModeParams === null ||
inputModeParams.command === null ||
!("validation" in inputModeParams.command)
) {
return;
}

const originalInput = (e as InputEvent).target as HTMLInputElement;
const currentValue = originalInput.value;
let checkValue: unknown = currentValue;
const command =
inputModeParams.command as CommandWithValidation<unknown>;
input.addEventListener("input", async (e) => {
if (
inputModeParams === null ||
inputModeParams.command === null ||
!("validation" in inputModeParams.command)
) {
return;
}

if ("inputValueConvert" in command) {
checkValue = command.inputValueConvert(currentValue);
}
await isValid(
checkValue,
currentValue,
originalInput,
command.validation
const handler = handlersCache.get(inputModeParams.command.id);
if (handler === undefined) {
throw new Error(
`Expected handler for command ${inputModeParams.command.id} is missing`
);
})
);
}

await handler(e);
});

modalEl.addEventListener("mousemove", (_e) => {
mouseMode = true;
Expand Down
18 changes: 2 additions & 16 deletions frontend/src/ts/commandline/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Config } from "@monkeytype/schemas/configs";
import AnimatedModal from "../utils/animated-modal";
import { z } from "zod";
import { Validation } from "../elements/input-validation";

// this file is needed becauase otherwise it would produce a circular dependency

Expand Down Expand Up @@ -47,21 +47,7 @@ export type CommandWithValidation<T> = (T extends string
* If the schema is defined it is always checked first.
* Only if the schema validaton is passed or missing the `isValid` method is called.
*/
validation: {
/**
* Zod schema to validate the input value against.
* The indicator will show the error messages from the schema.
*/
schema?: z.Schema<T>;
/**
* Custom async validation method.
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
* @param value current input value
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: T) => Promise<true | string>;
};
validation: Validation<T>;
};

export type CommandsSubgroup = {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/ts/constants/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const LanguageGroups: Record<string, Language[]> = {
],
arabic: ["arabic", "arabic_10k"],
arabic_egypt: ["arabic_egypt", "arabic_egypt_1k"],
arabic_morocco: ["arabic_morocco"],
italian: [
"italian",
"italian_1k",
Expand Down Expand Up @@ -340,6 +341,7 @@ export const LanguageGroups: Record<string, Language[]> = {
"code_arduino",
"code_systemverilog",
"code_elixir",
"code_gleam",
"code_zig",
"code_gdscript",
"code_gdscript_2",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/ts/elements/account-settings/ape-key-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Ape from "../../ape";
import { ApeKey, ApeKeys } from "@monkeytype/schemas/ape-keys";
import { format } from "date-fns/format";
import { SimpleModal, TextArea } from "../../utils/simple-modal";

import { isAuthenticated } from "../../firebase";
const editApeKey = new SimpleModal({
id: "editApeKey",
title: "Edit Ape key",
Expand Down Expand Up @@ -160,6 +160,8 @@ let apeKeys: ApeKeys | null = {};
const element = $("#pageAccountSettings .tab[data-tab='apeKeys']");

async function getData(): Promise<boolean> {
if (!isAuthenticated()) return false;

showLoaderRow();
const response = await Ape.apeKeys.get();

Expand Down
1 change: 1 addition & 0 deletions frontend/src/ts/elements/input-indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class InputIndicator {
}

$(this.inputElement).css("padding-right", "2.1em");
this.parentElement.attr("data-indicator-status", optionId);
}

get(): keyof typeof this.options | null {
Expand Down
Loading
Loading