* web: small fixes for wdio and lint - Roll back another dependabot breaking change, this time to WebdriverIO - Remove the redundant scripts wrapping ESLint for Precommit mode. Access to those modes is available through the flags to the `./web/scripts/eslint.mjs` script. - Remove SonarJS checks until SonarJS is ESLint 9 compatible. - Minor nitpicking. * package-lock.json update * web: small fixes for wdio and lint **PLEASE** Stop trying to upgrade WebdriverIO following Dependabot's instructions. The changes between wdio8 and wdio9 are extensive enough to require a lot more manual intervention. The unit tests fail in wdio 9, with the testbed driver Wdio uses to compile content to push to the browser ([vite](https://vitejs.dev) complaining: ``` 2024-09-27T15:30:03.672Z WARN @wdio/browser-runner:vite: warning: Unrecognized default export in file /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css Plugin: postcss-lit File: /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css [0-6] 2024-09-27T15:30:04.083Z INFO webdriver: BIDI COMMAND script.callFunction {"functionDeclaration":"<Function[976 bytes]>","awaitPromise":true,"arguments":[],"target":{"context":"8E608E6D13E355DFFC28112C236B73AF"}} [0-6] Error: Test failed due to following error(s): - ak-search-select.test.ts: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default': SyntaxError: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default' ``` So until we can figure out why the Vite installation isn't liking our CSS import scheme, we'll have to soldier on with what we have. At least with Wdio 8, we get: ``` Spec Files: 7 passed, 7 total (100% completed) in 00:00:19 ``` * Forgot to run prettier. * web: small fixes for elements and forms - provides a new utility, `_isSlug_`, used to verify a user input - extends the ak-horizontal-component wrapper to have a stronger identity and available value - updates the types that use the wrapper to be typed more strongly - (Why) The above are used in the wizard to get and store values - fixes a bug in SearchSelectEZ that broke the display if the user didn't supply a `groupBy` field. - Adds `@wdio/types` to the package file so eslint is satisfied wdio builds correctly - updates the end-to-end test to understand the revised button identities on the login page - Running the end-to-end tests verifies that changes to the components listed above did not break the semantics of those components. * Removing SonarJS comments. * Reverting to log level for tests.
181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
|
|
|
import { CSSResult, css } from "lit";
|
|
|
|
export function getCookie(name: string): string {
|
|
let cookieValue = "";
|
|
if (document.cookie && document.cookie !== "") {
|
|
const cookies = document.cookie.split(";");
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
// Does this cookie string begin with the name we want?
|
|
if (cookie.substring(0, name.length + 1) === name + "=") {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
|
|
export function convertToSlug(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/ /g, "-")
|
|
.replace(/[^\w-]+/g, "");
|
|
}
|
|
|
|
export function isSlug(text: string): boolean {
|
|
const lowered = text.toLowerCase();
|
|
const forbidden = /([^\w-]|\s)/.test(lowered);
|
|
return lowered === text && !forbidden;
|
|
}
|
|
|
|
/**
|
|
* Truncate a string based on maximum word count
|
|
*/
|
|
export function truncateWords(string: string, length = 10): string {
|
|
string = string || "";
|
|
const array = string.trim().split(" ");
|
|
const ellipsis = array.length > length ? "..." : "";
|
|
|
|
return array.slice(0, length).join(" ") + ellipsis;
|
|
}
|
|
|
|
/**
|
|
* Truncate a string based on character count
|
|
*/
|
|
export function truncate(string: string, length = 10): string {
|
|
return string.length > length ? `${string.substring(0, length)}...` : string;
|
|
}
|
|
|
|
export function camelToSnake(key: string): string {
|
|
const result = key.replace(/([A-Z])/g, " $1");
|
|
return result.split(" ").join("_").toLowerCase();
|
|
}
|
|
|
|
const capitalize = (key: string) => (key.length === 0 ? "" : key[0].toUpperCase() + key.slice(1));
|
|
|
|
export function snakeToCamel(key: string) {
|
|
const [start, ...rest] = key.split("_");
|
|
return [start, ...rest.map(capitalize)].join("");
|
|
}
|
|
|
|
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
|
const m = new Map<string, T[]>();
|
|
objects.forEach((obj) => {
|
|
const group = callback(obj);
|
|
if (!m.has(group)) {
|
|
m.set(group, []);
|
|
}
|
|
const tProviders = m.get(group) || [];
|
|
tProviders.push(obj);
|
|
});
|
|
return Array.from(m).sort();
|
|
}
|
|
|
|
export function first<T>(...args: Array<T | undefined | null>): T {
|
|
for (let index = 0; index < args.length; index++) {
|
|
const element = args[index];
|
|
if (element !== undefined && element !== null) {
|
|
return element;
|
|
}
|
|
}
|
|
throw new SentryIgnoredError(`No compatible arg given: ${args}`);
|
|
}
|
|
|
|
// Taken from python's string module
|
|
export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
|
|
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
export const ascii_letters = ascii_lowercase + ascii_uppercase;
|
|
export const digits = "0123456789";
|
|
export const hexdigits = digits + "abcdef" + "ABCDEF";
|
|
export const octdigits = "01234567";
|
|
export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
|
|
|
export function randomString(len: number, charset: string): string {
|
|
const chars = [];
|
|
const array = new Uint8Array(len);
|
|
self.crypto.getRandomValues(array);
|
|
for (let index = 0; index < len; index++) {
|
|
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
|
|
}
|
|
return chars.join("");
|
|
}
|
|
|
|
export function dateTimeLocal(date: Date): string {
|
|
// So for some reason, the datetime-local input field requires ISO Datetime as value
|
|
// But the standard javascript date.toISOString() returns everything with seconds and
|
|
// milliseconds, which the input field doesn't like (on chrome, on firefox its fine)
|
|
// On chrome, setting .valueAsNumber works, but that causes an error on firefox, so go
|
|
// figure.
|
|
// Additionally, toISOString always returns the date without timezone, which we would like
|
|
// to include for better usability
|
|
const tzOffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
|
|
const localISOTime = new Date(date.getTime() - tzOffset).toISOString().slice(0, -1);
|
|
const parts = localISOTime.split(":");
|
|
return `${parts[0]}:${parts[1]}`;
|
|
}
|
|
|
|
export function dateToUTC(date: Date): Date {
|
|
// Sigh...so our API is UTC/can take TZ info in the ISO format as it should.
|
|
// datetime-local fields (which is almost the only date-time input we use)
|
|
// can return its value as a UTC timestamp...however the generated API client
|
|
// _requires_ a Date object, only to then convert it to an ISO string anyways
|
|
// JS Dates don't include timezone info in the ISO string, so that just sends
|
|
// the local time as UTC...which is wrong
|
|
// Instead we have to do this, convert the given date to a UTC timestamp,
|
|
// then subtract the timezone offset to create an "invalid" date (correct time&date)
|
|
// but it still "thinks" it's in local TZ
|
|
const timestamp = date.getTime();
|
|
const offset = -1 * (new Date().getTimezoneOffset() * 60000);
|
|
return new Date(timestamp - offset);
|
|
}
|
|
|
|
// Lit is extremely well-typed with regard to CSS, and Storybook's `build` does not currently have a
|
|
// coherent way of importing CSS-as-text into CSSStyleSheet. It works well when Storybook is running
|
|
// in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports
|
|
// using the function below.
|
|
type AdaptableStylesheet = Readonly<string | CSSResult | CSSStyleSheet>;
|
|
type AdaptedStylesheets = CSSStyleSheet | CSSStyleSheet[];
|
|
|
|
const isCSSResult = (v: unknown): v is CSSResult =>
|
|
v instanceof CSSResult && v.styleSheet !== undefined;
|
|
|
|
// prettier-ignore
|
|
export const _adaptCSS = (sheet: AdaptableStylesheet): CSSStyleSheet =>
|
|
(typeof sheet === "string" ? css([sheet] as unknown as TemplateStringsArray, []).styleSheet
|
|
: isCSSResult(sheet) ? sheet.styleSheet
|
|
: sheet) as CSSStyleSheet;
|
|
|
|
// Overloaded function definitions inform consumers that if you pass it an array, expect an array in
|
|
// return; if you pass it a scaler, expect a scalar in return.
|
|
|
|
export function adaptCSS(sheet: AdaptableStylesheet): CSSStyleSheet;
|
|
export function adaptCSS(sheet: AdaptableStylesheet[]): CSSStyleSheet[];
|
|
export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): AdaptedStylesheets {
|
|
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
|
|
}
|
|
|
|
const _timeUnits = new Map<Intl.RelativeTimeFormatUnit, number>([
|
|
["year", 24 * 60 * 60 * 1000 * 365],
|
|
["month", (24 * 60 * 60 * 1000 * 365) / 12],
|
|
["day", 24 * 60 * 60 * 1000],
|
|
["hour", 60 * 60 * 1000],
|
|
["minute", 60 * 1000],
|
|
["second", 1000],
|
|
]);
|
|
|
|
export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
|
|
const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" });
|
|
const elapsed = d1.getTime() - d2.getTime();
|
|
|
|
// "Math.abs" accounts for both "past" & "future" scenarios
|
|
for (const [key, value] of _timeUnits) {
|
|
if (Math.abs(elapsed) > value || key == "second") {
|
|
return rtf.format(Math.round(elapsed / value), key);
|
|
}
|
|
}
|
|
return rtf.format(Math.round(elapsed / 1000), "second");
|
|
}
|