Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New DB connection form #3319

Merged
merged 13 commits into from
Jan 8, 2024
Merged
2 changes: 2 additions & 0 deletions mathesar_ui/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ module.exports = {
'DEFAULT',
'Mathesar.org',
'NULL',
'@',
'/',
'*',
'+',
':',
Expand Down
2 changes: 1 addition & 1 deletion mathesar_ui/src/AppTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TreeItem } from '@mathesar-component-library/types';

/** @deprecated Use either Connection or ConnectionModel interface instead */
/** @deprecated in favor of Connection */
export interface Database {
id: number;
nickname: string;
Expand Down
72 changes: 70 additions & 2 deletions mathesar_ui/src/api/connections.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
addQueryParamsToUrl,
deleteAPI,
getAPI,
patchAPI,
postAPI,
type PaginatedResponse,
deleteAPI,
addQueryParamsToUrl,
} from './utils/requestUtils';

export interface Connection {
Expand All @@ -27,6 +28,70 @@ function list() {
);
}

export const sampleDataOptions = [
'library_management',
'movie_collection',
] as const;

export type SampleDataSchemaIdentifier = (typeof sampleDataOptions)[number];

export type ConnectionReference =
| { connection_type: 'internal_database' }
| { connection_type: 'user_database'; id: Connection['id'] };

export interface CommonCreationProps {
database_name: string;
sample_data: SampleDataSchemaIdentifier[];
nickname: string;
}

export interface CreateFromKnownConnectionProps extends CommonCreationProps {
credentials: { connection: ConnectionReference };
create_database: boolean;
}

/**
* The `create_database` field is not present on this variant because we do not
* support creating the database in this case. The reason for this restriction
* is that we need a valid connection to an existing database in order to first
* create one. In theory, we could ask the user to supply such a connection but
* Sean and Brent deemed that approach to add too much additional complexity to
* the UI to justify its inclusion. We predict that if someone already has a
* PostgreSQL user, they are likely to already have a PostgreSQL database too.
*/
export interface CreateFromScratchProps extends CommonCreationProps {
credentials: {
user: string;
password: string;
host: string;
port: string;
};
}

export interface CreateWithNewUserProps extends CommonCreationProps {
credentials: {
create_user_via: ConnectionReference;
user: string;
password: string;
};
create_database: boolean;
}

function createFromKnownConnection(props: CreateFromKnownConnectionProps) {
const url = '/api/ui/v0/connections/create_from_known_connection/';
return postAPI<Connection>(url, props);
}

function createFromScratch(props: CreateFromScratchProps) {
const url = '/api/ui/v0/connections/create_from_scratch/';
return postAPI<Connection>(url, props);
}

function createWithNewUser(props: CreateWithNewUserProps) {
const url = '/api/ui/v0/connections/create_with_new_user/';
return postAPI<Connection>(url, props);
}

function update(
connectionId: Connection['id'],
properties: Partial<UpdatableConnectionProperties>,
Expand All @@ -51,6 +116,9 @@ function deleteConnection(

export default {
list,
createFromKnownConnection,
createFromScratch,
createWithNewUser,
update,
delete: deleteConnection,
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
<script lang="ts">
import FieldsetGroup from '@mathesar-component-library-dir/fieldset-group/FieldsetGroup.svelte';
import type { SvelteComponent } from 'svelte';

import Checkbox from '@mathesar-component-library-dir/checkbox/Checkbox.svelte';
import type { LabelGetter } from '@mathesar-component-library-dir/common/utils/formatUtils';
import FieldsetGroup from '@mathesar-component-library-dir/fieldset-group/FieldsetGroup.svelte';
import type { ComponentWithProps } from '../types';

type Option = $$Generic;

export let values: Option[] = [];
export let isInline = false;
export let options: Option[] = [];
export let options: readonly Option[] = [];
export let label: string | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let checkboxLabelKey: string | undefined = undefined;
export let getCheckboxLabel: LabelGetter<Option> | undefined = undefined;
export let getCheckboxHelp: <C extends SvelteComponent>(
value: Option,
) => string | ComponentWithProps<C> | undefined = () => undefined;
export let getCheckboxDisabled: (value: Option | undefined) => boolean = () =>
false;
/**
* By default, options will be compared by equality. If you're using objects as
* options, you can supply a custom function here to compare them.
Expand Down Expand Up @@ -41,12 +50,15 @@
{isInline}
{options}
{label}
{ariaLabel}
{disabled}
let:option
let:disabled={innerDisabled}
on:change
labelKey={checkboxLabelKey}
getLabel={getCheckboxLabel}
getHelp={getCheckboxHelp}
getDisabled={getCheckboxDisabled}
>
<Checkbox
on:change={({ detail: checked }) => handleChange(option, checked)}
Expand Down
10 changes: 10 additions & 0 deletions mathesar_ui/src/component-library/common/utils/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ export function requiredNonNullable<T>(
return defaultValue;
}

/**
* Used to make sure that a value is defined before using it.
*/
export function defined<T, U>(
v: T | undefined,
f: (v: T) => U | undefined,
): U | undefined {
return v === undefined ? undefined : f(v);
}

/**
* From https://stackoverflow.com/a/51365037/895563
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.fieldset-group {
padding: 0;
margin: 0;
--spacing-y-default: 0.5em;
--spacing-x-default: 1em;

Expand All @@ -14,10 +15,15 @@
&.has-label .options {
margin-top: 1em;
}
&:not(.inline) .option {
&:not(.inline) .option:not(:first-child) {
margin-top: var(--spacing-y, var(--spacing-y-default));
}
&:not(.inline) .option:not(:last-child) {
margin-bottom: var(--spacing-y, var(--spacing-y-default));
}
&:not(.inline) .option.has-help:not(:last-child) {
margin-bottom: calc(2 * var(--spacing-y, var(--spacing-y-default)));
}

&.inline .options {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
<script lang="ts">
import LabeledInput from '@mathesar-component-library-dir/labeled-input/LabeledInput.svelte';
import type { SvelteComponent } from 'svelte';

import type { LabelGetter } from '@mathesar-component-library-dir/common/utils/formatUtils';
import { getLabel as defaultGetLabel } from '@mathesar-component-library-dir/common/utils/formatUtils';
import LabeledInput from '@mathesar-component-library-dir/labeled-input/LabeledInput.svelte';
import Render from '@mathesar-component-library-dir/render/Render.svelte';
import StringOrComponent from '@mathesar-component-library-dir/string-or-component/StringOrComponent.svelte';
import type { ComponentWithProps } from '../types';

type Option = $$Generic;

export let isInline = false;
export let options: Option[] = [];
export let options: readonly Option[] = [];
export let label: string | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let disabled = false;
export let labelKey = 'label';
export let getLabel: LabelGetter<Option> = (o: Option) =>
defaultGetLabel(o, labelKey);
export let boxed = false;

export let getDisabled: (value: Option | undefined) => boolean = () => false;
export let getHelp: <C extends SvelteComponent>(
value: Option,
) => string | ComponentWithProps<C> | undefined = () => undefined;
</script>

<fieldset
Expand All @@ -35,12 +41,12 @@
{/if}
<ul class="options">
{#each options as option (option)}
<li class="option">
{@const help = getHelp(option)}
<li class="option" class:has-help={!!help}>
<LabeledInput layout="inline-input-first">
<svelte:fragment slot="label">
<StringOrComponent arg={getLabel(option)} />
</svelte:fragment>
<StringOrComponent slot="label" arg={getLabel(option)} />
<slot {option} disabled={getDisabled(option) || disabled} />
<Render slot="help" arg={help} />
</LabeledInput>
</li>
{/each}
Expand Down
1 change: 1 addition & 0 deletions mathesar_ui/src/component-library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export { default as PasswordInput } from './password-input/PasswordInput.svelte'
export { default as Progress } from './progress/Progress.svelte';
export { default as Radio } from './radio/Radio.svelte';
export { default as RadioGroup } from './radio-group/RadioGroup.svelte';
export { default as Render } from './render/Render.svelte';
export { default as Skeleton } from './skeleton/Skeleton.svelte';
export { default as Spinner } from './spinner/Spinner.svelte';
export { default as SpinnerArea } from './spinner-area/SpinnerArea.svelte';
Expand Down
28 changes: 15 additions & 13 deletions mathesar_ui/src/component-library/labeled-input/LabeledInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
}

.help {
// TODO figure out how we want to support help text with an inline layout.
// This CSS assumes the layout is stacked
display: block;
font-size: var(--text-size-small);
color: var(--color-text-muted);
Expand All @@ -34,29 +32,33 @@
}
}

&.layout-inline .label-content,
&.layout-inline-input-first .label-content {
// TODO: add support for help text with an inline layout.
&.layout-inline .label-content {
display: inline-flex;
flex-direction: row;
align-items: center; // To support a text input that's taller than the label
.label {
flex: 1 1 auto;
}
.input {
flex: 0 0 auto;
}
}

&.layout-inline .label-content {
flex-direction: row;
align-items: center; // To support a text input that's taller than the label
.input {
margin-left: var(--spacing-x, var(--spacing-x-default));
}
}

&.layout-inline-input-first .label-content {
flex-direction: row-reverse;
align-items: flex-start; // To support a checkbox with a wrapping label
display: grid;
grid-template: auto auto / auto 1fr;
.input {
grid-area: 1 / 1 / 1 / 1;
margin-right: var(--spacing-x, var(--spacing-x-default));
}
.label {
grid-area: 1 / 2 / 1 / 2;
}
.help {
grid-area: 2 / 2 / 2 / 2;
margin-top: var(--spacing-y, var(--spacing-y-default));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
>
<Label>
<span class="label-content">
<span class="label">
{label ?? ''}
<slot name="label" />
</span>
<span class="help">
{help ?? ''}
<slot name="help" />
</span>
<!--
⚠️ NOTE: Do not add any white space within `.label` or `.help`. We have
CSS that uses the `:empty` pseudo-class which does not work if there is
white space.
-->
<span class="label">{label ?? ''}<slot name="label" /></span>
<span class="help">{help ?? ''}<slot name="help" /></span>
<span class="input"><slot /></span>
</span>
</Label>
Expand Down
2 changes: 1 addition & 1 deletion mathesar_ui/src/component-library/list-box/ListBoxTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ListBoxStaticContextProps<Option> {
export interface ListBoxProps<Option>
extends Partial<ListBoxStaticContextProps<Option>> {
labelKey?: string;
options: Option[] | (() => CancellablePromise<Option[]>);
options: readonly Option[] | (() => CancellablePromise<Option[]>);
value?: Option[];
}

Expand Down
16 changes: 16 additions & 0 deletions mathesar_ui/src/component-library/margin-trim/MarginTrim.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--
@component Trims the outer margins of its children.
-->

<div class="margin-trim">
<slot />
</div>

<style>
.margin-trim > :first-child {
margin-top: 0;
}
.margin-trim > :last-child {
margin-bottom: 0;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

export let value: Option | undefined = undefined;
export let isInline = false;
export let options: Option[] = [];
export let options: readonly Option[] = [];
export let label: string | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let radioLabelKey: string | undefined = undefined;
Expand Down
32 changes: 32 additions & 0 deletions mathesar_ui/src/component-library/render/Render.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import type { SvelteComponent } from 'svelte';

import MarginTrim from '@mathesar-component-library-dir/margin-trim/MarginTrim.svelte';
import type { ComponentWithProps } from '@mathesar-component-library-dir/types';
import RenderComponentWithProps from './RenderComponentWithProps.svelte';

type T = $$Generic<SvelteComponent>;

export let arg: string | string[] | ComponentWithProps<T> | undefined =
undefined;
</script>

{#if arg === undefined}
{''}
{:else if typeof arg === 'string'}
{arg}
{:else if Array.isArray(arg)}
{#if arg.length === 0}
{''}
{:else if arg.length === 1}
{arg[0]}
{:else}
<MarginTrim>
{#each arg as paragraph}
<p>{paragraph}</p>
{/each}
</MarginTrim>
{/if}
{:else}
<RenderComponentWithProps componentWithProps={arg} />
{/if}
Loading
Loading