diff --git a/gitbutler-ui/src/lib/backend/auth.ts b/gitbutler-ui/src/lib/backend/auth.ts new file mode 100644 index 000000000..1e969ac75 --- /dev/null +++ b/gitbutler-ui/src/lib/backend/auth.ts @@ -0,0 +1,12 @@ +import { invoke } from './ipc'; + +export type GitCredentialCheck = { + error?: string; + ok: boolean; +}; + +export class AuthService { + async getPublicKey() { + return await invoke('get_public_key'); + } +} diff --git a/gitbutler-ui/src/lib/backend/projects.ts b/gitbutler-ui/src/lib/backend/projects.ts index 2afbdfeb1..352a211b7 100644 --- a/gitbutler-ui/src/lib/backend/projects.ts +++ b/gitbutler-ui/src/lib/backend/projects.ts @@ -16,13 +16,12 @@ import { get } from 'svelte/store'; import type { Project as CloudProject } from '$lib/backend/cloud'; import { goto } from '$app/navigation'; -export type Key = - | 'default' - | 'generated' - | 'gitCredentialsHelper' - | { - local: { private_key_path: string; passphrase?: string }; - }; +export type KeyType = 'default' | 'generated' | 'gitCredentialsHelper' | 'local'; +export type LocalKey = { + local: { private_key_path: string; passphrase?: string }; +}; + +export type Key = Exclude | LocalKey; export type Project = { id: string; diff --git a/gitbutler-ui/src/lib/components/AnalyticsSettings.svelte b/gitbutler-ui/src/lib/components/AnalyticsSettings.svelte index 38e498210..f2ee51a23 100644 --- a/gitbutler-ui/src/lib/components/AnalyticsSettings.svelte +++ b/gitbutler-ui/src/lib/components/AnalyticsSettings.svelte @@ -1,7 +1,7 @@ - - - - diff --git a/gitbutler-ui/src/lib/components/CloudForm.svelte b/gitbutler-ui/src/lib/components/CloudForm.svelte index 55bc32bae..9cf4051d3 100644 --- a/gitbutler-ui/src/lib/components/CloudForm.svelte +++ b/gitbutler-ui/src/lib/components/CloudForm.svelte @@ -1,6 +1,6 @@ {#if user}
- + Enable branch and commit message generation Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the @@ -71,9 +71,9 @@ - + - + Automatically generate branch names - +
@@ -90,14 +90,14 @@ {#if user.role === 'admin'}

Full data synchronization

- + onSyncChange(e.detail)} orientation="row"> Sync my history, repository and branch data for backup, sharing and team features. - + onSyncChange(e.detail)} /> - + {#if project.api} diff --git a/gitbutler-ui/src/lib/components/PreferencesForm.svelte b/gitbutler-ui/src/lib/components/PreferencesForm.svelte index e25cf610d..137374545 100644 --- a/gitbutler-ui/src/lib/components/PreferencesForm.svelte +++ b/gitbutler-ui/src/lib/components/PreferencesForm.svelte @@ -1,6 +1,6 @@
- { - allowForcePushing = !allowForcePushing; - onAllowForcePushingChange(); - }} - > + Allow force pushing Force pushing allows GitButler to override branches even if they were pushed to remote. We will never force push to the trunk. - + dispatch('updated', { ok_with_force_push: allowForcePushing })} + /> - + - { - omitCertificateCheck = !omitCertificateCheck; - onOmitCertificateCheckChange(); - }} - > + Ignore host certificate checks Enabling this will ignore host certificate checks when authenticating with ssh. - + dispatch('updated', { omit_certificate_check: omitCertificateCheck })} + /> - + - + Run commit hooks Enabling this will run any git pre and post commit hooks you have configured in your repository. - + - +
diff --git a/gitbutler-ui/src/lib/components/RadioButton.svelte b/gitbutler-ui/src/lib/components/RadioButton.svelte index 13fd89f76..5a128ac93 100644 --- a/gitbutler-ui/src/lib/components/RadioButton.svelte +++ b/gitbutler-ui/src/lib/components/RadioButton.svelte @@ -3,8 +3,8 @@ export let small = false; export let disabled = false; - export let group = ''; export let value = ''; + export let id = ''; diff --git a/gitbutler-ui/src/lib/components/Toggle.svelte b/gitbutler-ui/src/lib/components/Toggle.svelte index 024c0c192..58bc7a9b6 100644 --- a/gitbutler-ui/src/lib/components/Toggle.svelte +++ b/gitbutler-ui/src/lib/components/Toggle.svelte @@ -9,6 +9,7 @@ export let checked = false; export let value = ''; export let help = ''; + export let id = ''; let input: HTMLInputElement; const dispatch = createEventDispatcher<{ change: boolean }>(); @@ -26,7 +27,7 @@ class:small {value} {name} - id={name} + {id} {disabled} use:tooltip={help} /> diff --git a/gitbutler-ui/src/routes/+layout.ts b/gitbutler-ui/src/routes/+layout.ts index d4a9a09c6..9c377e7a9 100644 --- a/gitbutler-ui/src/routes/+layout.ts +++ b/gitbutler-ui/src/routes/+layout.ts @@ -1,5 +1,6 @@ import { initPostHog } from '$lib/analytics/posthog'; import { initSentry } from '$lib/analytics/sentry'; +import { AuthService } from '$lib/backend/auth'; import { getCloudApiClient } from '$lib/backend/cloud'; import { ProjectService } from '$lib/backend/projects'; import { UpdaterService } from '$lib/backend/updater'; @@ -33,7 +34,6 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet if (enabled) initPostHog(); }); const userService = new UserService(); - const updaterService = new UpdaterService(); // TODO: Find a workaround to avoid this dynamic import // https://github.com/sveltejs/kit/issues/905 @@ -41,9 +41,10 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet const defaultPath = await homeDir(); return { + authService: new AuthService(), projectService: new ProjectService(defaultPath), cloud: getCloudApiClient({ fetch: realFetch }), - updaterService, + updaterService: new UpdaterService(), userService, user$: userService.user$ }; diff --git a/gitbutler-ui/src/routes/[projectId]/+layout.ts b/gitbutler-ui/src/routes/[projectId]/+layout.ts index a57ea0f61..5bbcf64e7 100644 --- a/gitbutler-ui/src/routes/[projectId]/+layout.ts +++ b/gitbutler-ui/src/routes/[projectId]/+layout.ts @@ -11,7 +11,7 @@ import type { LayoutLoad } from './$types'; export const prerender = false; export const load: LayoutLoad = async ({ params, parent }) => { - const { user$, projectService, userService } = await parent(); + const { authService, projectService, userService } = await parent(); const projectId = params.projectId; const project$ = projectService.getProject(projectId); const fetches$ = getFetchNotifications(projectId); @@ -41,8 +41,11 @@ export const load: LayoutLoad = async ({ params, parent }) => { branchController ); + const user$ = userService.user$; + return { projectId, + authService, branchController, baseBranchService, githubService, diff --git a/gitbutler-ui/src/routes/[projectId]/settings/+page.svelte b/gitbutler-ui/src/routes/[projectId]/settings/+page.svelte index aad799416..5daaa4630 100644 --- a/gitbutler-ui/src/routes/[projectId]/settings/+page.svelte +++ b/gitbutler-ui/src/routes/[projectId]/settings/+page.svelte @@ -8,7 +8,6 @@ import SectionCard from '$lib/components/SectionCard.svelte'; import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte'; import * as toasts from '$lib/utils/toasts'; - import type { UserError } from '$lib/backend/ipc'; import type { Key, Project } from '$lib/backend/projects'; import type { PageData } from './$types'; import { goto } from '$app/navigation'; @@ -24,34 +23,41 @@ let deleteConfirmationModal: RemoveProjectButton; let isDeleting = false; - const onDeleteClicked = () => - Promise.resolve() - .then(() => (isDeleting = true)) - .then(() => projectService.deleteProject($project$?.id)) - .catch((e) => { - console.error(e); - toasts.error('Failed to delete project'); - }) - .then(() => toasts.success('Project deleted')) - .then(() => goto('/')) - .finally(() => { - isDeleting = false; - projectService.reload(); - }); + async function onDeleteClicked() { + isDeleting = true; + try { + projectService.deleteProject($project$?.id); + toasts.success('Project deleted'); + goto('/'); + } catch (err: any) { + console.error(err); + toasts.error('Failed to delete project'); + } finally { + isDeleting = false; + projectService.reload(); + } + } - const onKeysUpdated = (e: { detail: { preferred_key: Key } }) => - projectService - .updateProject({ ...$project$, ...e.detail }) - .then(() => toasts.success('Preferred key updated')) - .catch((e: UserError) => { - toasts.error(e.message); - }); - const onCloudUpdated = (e: { detail: Project }) => + async function onKeysUpdated(e: { detail: { preferred_key: Key } }) { + try { + projectService.updateProject({ ...$project$, ...e.detail }); + toasts.success('Preferred key updated'); + } catch (err: any) { + toasts.error(err.message); + } + } + + async function onCloudUpdated(e: { detail: Project }) { projectService.updateProject({ ...$project$, ...e.detail }); - const onPreferencesUpdated = (e: { + } + + async function onPreferencesUpdated(e: { detail: { ok_with_force_push?: boolean; omit_certificate_check?: boolean }; - }) => projectService.updateProject({ ...$project$, ...e.detail }); - const onDetailsUpdated = async (e: { detail: Project }) => { + }) { + await projectService.updateProject({ ...$project$, ...e.detail }); + } + + async function onDetailsUpdated(e: { detail: Project }) { const api = $user$ && e.detail.api ? await cloud.projects.update($user$?.access_token, e.detail.api.repository_id, { @@ -59,12 +65,11 @@ description: e.detail.description }) : undefined; - projectService.updateProject({ ...e.detail, api: api ? { ...api, sync: e.detail.api?.sync || false } : undefined }); - }; + } {#if !$project$} diff --git a/gitbutler-ui/src/routes/settings/+page.svelte b/gitbutler-ui/src/routes/settings/+page.svelte index 7625cf171..d63d5426a 100644 --- a/gitbutler-ui/src/routes/settings/+page.svelte +++ b/gitbutler-ui/src/routes/settings/+page.svelte @@ -2,7 +2,6 @@ import { deleteAllData } from '$lib/backend/data'; import AnalyticsSettings from '$lib/components/AnalyticsSettings.svelte'; import Button from '$lib/components/Button.svelte'; - import ClickableCard from '$lib/components/ClickableCard.svelte'; import GithubIntegration from '$lib/components/GithubIntegration.svelte'; import Link from '$lib/components/Link.svelte'; import Login from '$lib/components/Login.svelte'; @@ -19,37 +18,33 @@ import * as toasts from '$lib/utils/toasts'; import { openExternalUrl } from '$lib/utils/url'; import { invoke } from '@tauri-apps/api/tauri'; + import { onMount } from 'svelte'; import type { PageData } from './$types'; import { goto } from '$app/navigation'; export let data: PageData; - const { cloud, user$, userService } = data; - - $: saving = false; - - $: userPicture = $user$?.picture; + const { cloud, user$, userService, authService } = data; const fileTypes = ['image/jpeg', 'image/png']; - const validFileType = (file: File) => { - return fileTypes.includes(file.type); - }; - - const onPictureChange = (e: Event) => { - const target = e.target as HTMLInputElement; - const file = target.files?.[0]; - - if (file && validFileType(file)) { - userPicture = URL.createObjectURL(file); - } else { - userPicture = $user$?.picture; - toasts.error('Please use a valid image file'); - } - }; + // TODO: Maybe break these into components? + let currentSection: 'profile' | 'git-stuff' | 'telemetry' | 'integrations' = 'profile'; + // TODO: Refactor such that this variable isn't needed let newName = ''; let loaded = false; + let isDeleting = false; + + let signCommits = false; + let annotateCommits = true; + let sshKey = ''; + + let deleteConfirmationModal: Modal; + + $: saving = false; + $: userPicture = $user$?.picture; + $: if ($user$ && !loaded) { loaded = true; cloud.user.get($user$?.access_token).then((cloudUser) => { @@ -59,7 +54,19 @@ newName = $user$?.name || ''; } - const onSubmit = async (e: SubmitEvent) => { + function onPictureChange(e: Event) { + const target = e.target as HTMLInputElement; + const file = target.files?.[0]; + + if (file && fileTypes.includes(file.type)) { + userPicture = URL.createObjectURL(file); + } else { + userPicture = $user$?.picture; + toasts.error('Please use a valid image file'); + } + } + + async function onSubmit(e: SubmitEvent) { if (!$user$) return; saving = true; @@ -80,74 +87,55 @@ toasts.error('Failed to update user'); } saving = false; - }; + } - let isDeleting = false; - let deleteConfirmationModal: Modal; - - export function git_get_config(params: { key: string }) { + // TODO: These kinds of functions should be implemented on an injected service + function gitGetConfig(params: { key: string }) { return invoke('git_get_global_config', params); } - export function git_set_config(params: { key: string; value: string }) { + function gitSetConfig(params: { key: string; value: string }) { return invoke('git_set_global_config', params); } - const setCommitterSetting = (value: boolean) => { - annotateCommits = value; - git_set_config({ + function toggleCommitterSigning() { + annotateCommits = !annotateCommits; + gitSetConfig({ key: 'gitbutler.gitbutlerCommitter', - value: value ? '1' : '0' + value: annotateCommits ? '1' : '0' }); - }; - - const setSigningSetting = (value: boolean) => { - signCommits = value; - git_set_config({ - key: 'gitbutler.signCommits', - value: value ? 'true' : 'false' - }); - }; - - export function get_public_key() { - return invoke('get_public_key'); } - let sshKey = ''; - get_public_key().then((key) => { - sshKey = key; - }); + function toggleSigningSetting() { + signCommits = !signCommits; + gitSetConfig({ + key: 'gitbutler.signCommits', + value: signCommits ? 'true' : 'false' + }); + } - $: annotateCommits = true; - $: signCommits = false; - - git_get_config({ key: 'gitbutler.gitbutlerCommitter' }).then((value) => { - annotateCommits = value ? value === '1' : false; - }); - - git_get_config({ key: 'gitbutler.signCommits' }).then((value) => { - signCommits = value ? value === 'true' : false; - }); - - const onDeleteClicked = () => - Promise.resolve() - .then(() => (isDeleting = true)) - .then(() => deleteAllData()) + async function onDeleteClicked() { + isDeleting = true; + try { + deleteAllData(); + await userService.logout(); // TODO: Delete user from observable!!! - .then(() => userService.logout()) - .then(() => toasts.success('All data deleted')) - .catch((e) => { - console.error(e); - toasts.error('Failed to delete project'); - }) - .then(() => deleteConfirmationModal.close()) - .then(() => goto('/', { replaceState: true, invalidateAll: true })) - .finally(() => (isDeleting = false)); + toasts.success('All data deleted'); + goto('/', { replaceState: true, invalidateAll: true }); + } catch (err: any) { + console.error(err); + toasts.error('Failed to delete project'); + } finally { + deleteConfirmationModal.close(); + isDeleting = false; + } + } - let currentSection: 'profile' | 'git-stuff' | 'telemetry' | 'integrations' = 'profile'; - - const toggleGBCommiter = () => setCommitterSetting(!annotateCommits); - const toggleGBSigner = () => setSigningSetting(!signCommits); + onMount(async () => { + sshKey = await authService.getPublicKey(); + annotateCommits = (await gitGetConfig({ key: 'gitbutler.gitbutlerCommitter' })) == '1'; + signCommits = (await gitGetConfig({ key: 'gitbutler.signCommits' })) == 'true'; + });
@@ -233,7 +221,7 @@ {:else if currentSection === 'git-stuff'} - + Credit GitButler as the Committer By default, everything in the GitButler client is free to use. You can opt in to crediting @@ -247,9 +235,13 @@ - + - + @@ -283,7 +275,7 @@ - + Sign Commits with the above SSH Key If you want GitButler to sign your commits with the SSH key we generated, then you can add @@ -297,9 +289,9 @@ - + - + {:else if currentSection === 'telemetry'} diff --git a/gitbutler-ui/static/images/lock.svg b/gitbutler-ui/static/images/lock.svg new file mode 100644 index 000000000..49c147eaa --- /dev/null +++ b/gitbutler-ui/static/images/lock.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +