-
-
Notifications
You must be signed in to change notification settings - Fork 362
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
Homepage UI #3711
Homepage UI #3711
Changes from 14 commits
72e0258
0a16b44
716e981
b8aed1d
ddec88e
f3716d6
655fa2c
657e284
af451f5
ef8b431
2ea8ca7
98e7f00
0c68c81
6ea3d8b
aa42c52
eeb5547
25ff21c
e6a70d9
bba0a68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,17 +2,100 @@ import { rpcMethodTypeContainer } from '@mathesar/packages/json-rpc-client-build | |
|
||
import type { Server } from './servers'; | ||
|
||
export interface Database { | ||
export interface DatabaseResponse { | ||
id: number; | ||
name: string; | ||
server_id: Server['id']; | ||
} | ||
|
||
/** | ||
* TODO: Figure out a better place to move classes like these. | ||
* Perhaps `@mathesar/models/database`. | ||
* | ||
* Model classes should follow these rules: | ||
* - The class should be immutable. | ||
* - There should not be `undefined` properties. | ||
* - All properties should be readonly. | ||
* - All properties should be initialized in the constructor. | ||
* - If a property has to be changed, we should return a new class. | ||
* - Svelte stores should not be used inside Model classes. | ||
* - The Model classes themselves can be contained in a Svelte store. | ||
* | ||
* Good: | ||
* ``` | ||
* export class Model { | ||
* readonly proprety: string; | ||
* | ||
* constructor(value: string) { | ||
* this.proprety = value; | ||
* } | ||
* | ||
* fetchAnotherProperty(): string { | ||
* const anotherProperty = api.model.getAnotherProp(); | ||
* return anotherProperty; | ||
* } | ||
* | ||
* changeProperty(newValue: string): Model { | ||
* return new Model(newValue); | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* Bad: | ||
* ``` | ||
* export class Model { | ||
* // All properties should be readonly | ||
* proprety: string; | ||
* | ||
* // Do not have undefined properties | ||
* anotherProp: string | undefined = undefined; | ||
* | ||
* constructor(prop: string) { | ||
* this.proprety = prop; | ||
* } | ||
* | ||
* fetchAnotherProperty(): string { | ||
* // Do not cache here | ||
* this.anotherProp = api.model.getAnotherProp(); | ||
* return this.anotherProp; | ||
* } | ||
* | ||
* changeProperty(newValue: string): Model { | ||
* // Do not mutate propreties | ||
* this.property = newValue; | ||
* return this; | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
export class Database implements DatabaseResponse { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to put this code somewhere else, and I support putting it in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to make any change related to this after we figure out the store structures from the proposal PRs. |
||
readonly id: number; | ||
|
||
readonly name: string; | ||
|
||
readonly server_id: number; | ||
|
||
readonly server_host: string; | ||
|
||
readonly server_port: number; | ||
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to make any change related to this after we figure out the store structures from the proposal PRs. |
||
|
||
constructor(databaseResponse: DatabaseResponse, server: Server) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious why you chose a class for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could resolve this in the proposal PRs and on call. |
||
this.id = databaseResponse.id; | ||
this.name = databaseResponse.name; | ||
if (databaseResponse.server_id !== server.id) { | ||
throw new Error('Server ids do not match'); | ||
} | ||
this.server_id = databaseResponse.server_id; | ||
this.server_host = server.host; | ||
this.server_port = server.port; | ||
} | ||
} | ||
|
||
export const databases = { | ||
list: rpcMethodTypeContainer< | ||
{ | ||
server_id?: Database['server_id']; | ||
server_id?: DatabaseResponse['server_id']; | ||
}, | ||
Array<Database> | ||
Array<DatabaseResponse> | ||
>(), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
<script lang="ts"> | ||
import { iconMoreActions } from '@mathesar/icons'; | ||
import { | ||
DropdownMenu, | ||
makeStyleStringFromCssVariables, | ||
} from '@mathesar-component-library'; | ||
|
||
export let href: string; | ||
export let ariaLabel: string | undefined = undefined; | ||
export let cssVariables: Record<string, string> | undefined = undefined; | ||
|
||
let isHoveringMenuTrigger = false; | ||
let isCardFocused = false; | ||
|
||
$: style = cssVariables | ||
? makeStyleStringFromCssVariables(cssVariables) | ||
: undefined; | ||
</script> | ||
|
||
<div | ||
class="card" | ||
class:focus={isCardFocused} | ||
class:hovering-menu-trigger={isHoveringMenuTrigger} | ||
{style} | ||
> | ||
<a | ||
class="link passthrough" | ||
{href} | ||
aria-label={ariaLabel} | ||
on:focusin={() => { | ||
isCardFocused = true; | ||
}} | ||
on:focusout={() => { | ||
isCardFocused = false; | ||
}} | ||
> | ||
<div class="top"> | ||
<slot /> | ||
</div> | ||
{#if $$slots.description} | ||
<div class="description"> | ||
<slot name="description" /> | ||
</div> | ||
{/if} | ||
</a> | ||
{#if $$slots.menu} | ||
<div | ||
class="menu-container" | ||
on:mouseenter={() => { | ||
isHoveringMenuTrigger = true; | ||
}} | ||
on:mouseleave={() => { | ||
isHoveringMenuTrigger = false; | ||
}} | ||
> | ||
<DropdownMenu | ||
showArrow={false} | ||
triggerAppearance="ghost" | ||
triggerClass="dropdown-menu-button" | ||
closeOnInnerClick={true} | ||
placements={['bottom-end', 'right-start', 'left-start']} | ||
label="" | ||
icon={iconMoreActions} | ||
size="small" | ||
> | ||
<slot name="menu" /> | ||
</DropdownMenu> | ||
</div> | ||
{/if} | ||
{#if $$slots.footer} | ||
<div class="footer"> | ||
<slot name="footer" /> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<style> | ||
.card { | ||
position: relative; | ||
isolation: isolate; | ||
border-radius: var(--border-radius-l); | ||
border: 1px solid var(--slate-200); | ||
background-color: var(--white); | ||
--padding-v-internal: 1rem; | ||
--padding-h-internal: 1rem; | ||
} | ||
.card.focus { | ||
outline: 2px solid var(--slate-300); | ||
outline-offset: 1px; | ||
border-radius: var(--border-radius-l); | ||
} | ||
.link { | ||
display: grid; | ||
grid-template: auto 1fr auto / 1fr; | ||
border-radius: var(--border-radius-l); | ||
cursor: pointer; | ||
overflow: hidden; | ||
height: 100%; | ||
padding: var(--Card__padding-v, var(--padding-v-internal)) 0; | ||
} | ||
.link:hover { | ||
border-color: var(--slate-500); | ||
background-color: var(--slate-50); | ||
box-shadow: 0 0.2rem 0.4rem 0 rgba(0, 0, 0, 0.1); | ||
} | ||
.top { | ||
overflow: hidden; | ||
font-size: var(--text-size-large); | ||
height: var(--menu-trigger-size, auto); | ||
display: flex; | ||
align-items: center; | ||
padding: 0 var(--Card__padding-h, var(--padding-h-internal)); | ||
} | ||
.description:not(:empty) { | ||
padding: var(--size-x-small) | ||
var(--Card__padding-h, var(--padding-h-internal)) 0 | ||
var(--Card__padding-h, var(--padding-h-internal)); | ||
font-size: var(--text-size-base); | ||
} | ||
|
||
.menu-container { | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
margin: var(--size-ultra-small); | ||
z-index: 1; | ||
} | ||
.menu-container :global(.dropdown-menu-button) { | ||
width: 100%; | ||
height: 100%; | ||
font-size: var(--text-size-large); | ||
color: var(--slate-500); | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: center; | ||
} | ||
.menu-container :global(.dropdown-menu-button:hover) { | ||
color: var(--slate-800); | ||
background: var(--slate-100); | ||
} | ||
.footer { | ||
position: absolute; | ||
bottom: 0; | ||
right: 0; | ||
width: 100%; | ||
z-index: 1; | ||
cursor: pointer; | ||
} | ||
</style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea, but I have some different opinions regarding the rules. It would be nice to chat about this on a call.
Also, it's nice seeing this sort of proposal written down, but process-wise I don't like using code comments as a discussion channel. I'd prefer to use a wiki PR or github issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could resolve this in the proposal PRs and on call. I've removed the discussion code comments.