diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index c50462dba..b7b0e0cd9 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -377,6 +377,8 @@ async fn handle_ui_request( || resource_path == "/select" || resource_path == "/browse" || resource_path == "/view" + || resource_path == "/diff" + || resource_path == "/change" || resource_path == "/history"; let resolve_path = if is_app_route { diff --git a/josh-ui/src/App.scss b/josh-ui/src/App.scss index 5ca6d4ab5..e2d145b3f 100644 --- a/josh-ui/src/App.scss +++ b/josh-ui/src/App.scss @@ -20,7 +20,6 @@ $color-link-visited-hover: #ffffaa; &:hover { color: $color-link-hover; - text-decoration: underline; } &:visited { @@ -132,19 +131,48 @@ nav { } } -.commit-list-entry { +.file-browser-list-entry { @include ui-link-clickable; padding: .4em .4em; &:hover { background: $color-background-highlight; } +} + +.commit-list-entry-dir { + @include ui-link-clickable; + &:hover { + background: $color-background-highlight; + } +} + +.commit-list-entry-browse { + @include ui-link-clickable; + &:hover { + background: $color-background-highlight; + } +} + + +.commit-list-entry { + padding: .4em .4em; span.hash { margin: 0 0.7em 0 0; color: $color-highlight; font-weight: bolder; } + span.authorEmail { + margin: 0 0.7em 0 0; + color: $color-highlight; + font-weight: bolder; + } + span.summary { + display: block; + margin: 0 0.7em 0 0; + font-weight: bolder; + } } .ui-button { diff --git a/josh-ui/src/App.tsx b/josh-ui/src/App.tsx index 31dcb43d8..8b6cf9a88 100644 --- a/josh-ui/src/App.tsx +++ b/josh-ui/src/App.tsx @@ -19,6 +19,8 @@ import {RepoSelector} from './RepoSelector'; import {NavigateCallback, NavigateTarget, NavigateTargetType} from "./Navigation"; import {match} from "ts-pattern"; import {FileViewer} from "./FileViewer"; +import {DiffViewer} from "./DiffViewer"; +import {ChangeViewer} from "./ChangeViewer"; import {HistoryList} from "./History"; import {Breadcrumbs} from "./Breadcrumbs"; import {DEFAULT_FILTER} from "./Josh"; @@ -42,7 +44,9 @@ function useNavigateCallback(): NavigateCallback { const pathname = match(targetType) .with(NavigateTargetType.History, () => '/history') .with(NavigateTargetType.Directory, () => '/browse') + .with(NavigateTargetType.Change, () => '/change') .with(NavigateTargetType.File, () => '/view') + .with(NavigateTargetType.Diff, () => '/diff') .run() navigate({ @@ -152,6 +156,27 @@ function Browse() { } +function ChangeView() { + const param = useStrictGetSearchParam() + + useEffect(() => { + document.title = `/${param('path')} - ${param('repo')} - Josh` + }); + + return
+ + + +
+} + function History() { const param = useStrictGetSearchParam() @@ -201,6 +226,38 @@ function View() { ) } +function DiffView() { + const param = useStrictGetSearchParam() + + useEffect(() => { + document.title = `${param('path')} - ${param('repo')} - Josh` + }); + + return ( +
+ + + + + +
+ ) +} + + function App() { return ( @@ -210,6 +267,8 @@ function App() { } /> } /> } /> + } /> + } /> ); diff --git a/josh-ui/src/ChangeViewer.tsx b/josh-ui/src/ChangeViewer.tsx new file mode 100644 index 000000000..9c3cb70a6 --- /dev/null +++ b/josh-ui/src/ChangeViewer.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import {GraphQLClient} from 'graphql-request' +import {getServer} from "./Server"; +import {NavigateCallback, NavigateTargetType, QUERY_CHANGE} from "./Navigation"; +import {match} from "ts-pattern"; + +export type ChangeViewProps = { + repo: string + filter: string + rev: string + navigateCallback: NavigateCallback +} + +type Path = { + path: string +} + +type ChangedFile = { + from: Path + to: Path +} + +type State = { + summary: string + files: ChangedFile[] + client: GraphQLClient +} + +export class ChangeViewer extends React.Component { + state: State = { + summary: "", + files: [], + client: new GraphQLClient(`${getServer()}/~/graphql/${this.props.repo}`, { + mode: 'cors' + }), + }; + + startRequest() { + this.state.client.rawRequest(QUERY_CHANGE, { + rev: this.props.rev, + filter: this.props.filter, + }).then((d) => { + const data = d.data.rev + + this.setState({ + summary: data.summary, + files: data.changedFiles, + }) + }) + } + + componentDidMount() { + this.startRequest() + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { + if (prevProps !== this.props) { + this.setState({ + files: [], + }) + + this.startRequest() + } + } + + componentWillUnmount() { + // TODO cancel request? + } + + renderList(values: ChangedFile[], target: NavigateTargetType) { + const classNameSuffix = match(target) + .with(NavigateTargetType.Diff, () => 'file') + .run() + + const navigate = (path: string, e: React.MouseEvent) => { + this.props.navigateCallback(target, { + repo: this.props.repo, + filter: this.props.filter, + path: path, + rev: this.props.rev + }) + } + + return values.map((entry) => { + const className = `file-browser-list-entry file-browser-list-entry-${classNameSuffix}` + let path = ""; + let prefix = "M"; + if (!entry.from) { + prefix = "A"; + path = entry.to.path; + } + else if (!entry.to) { + prefix = "D"; + path = entry.from.path; + } + else { + path = entry.from.path; + } + + return
+ {prefix}{path} +
+ }) + } + + render() { + if (this.state.files.length === 0) { + return
Loading...
+ } else { + return
+
+ {this.state.summary} +
+
+ {this.renderList(this.state.files, NavigateTargetType.Diff)} +
+
+ } + } +} diff --git a/josh-ui/src/DiffViewer.tsx b/josh-ui/src/DiffViewer.tsx new file mode 100644 index 000000000..43667e7dc --- /dev/null +++ b/josh-ui/src/DiffViewer.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import {DiffEditor} from "@monaco-editor/react"; +import {NavigateCallback, QUERY_FILE_DIFF} from "./Navigation"; +import {GraphQLClient} from "graphql-request"; +import {getServer} from "./Server"; +import {match} from "ts-pattern"; + +export type DiffViewerProps = { + repo: string + path: string + filter: string + rev: string + navigateCallback: NavigateCallback +} + +type State = { + content_a?: string + content_b?: string + client: GraphQLClient +} + +function mapLanguage(path: string) { + const extension = path.split('.').pop() + + return match(extension) + .with('css', () => 'css') + .with('html', 'htm', 'xhtml', () => 'html') + .with('json', () => 'json') + .with('ts', 'ts.d', 'tsx', () => 'typescript') + .with('md', () => 'markdown') + .with('rs', () => 'rust') + .with('Dockerfile', () => 'dockerfile') + .otherwise(() => undefined) +} + +export class DiffViewer extends React.Component { + state = { + content_a: undefined, + content_b: undefined, + client: new GraphQLClient(`${getServer()}/~/graphql/${this.props.repo}`, { + mode: 'cors', + errorPolicy: 'all' + }), + } + + componentDidMount() { + this.state.client.rawRequest(QUERY_FILE_DIFF, { + rev: this.props.rev, + filter: this.props.filter, + path: this.props.path, + }).then((d) => { + const data = d.data.rev + + let content_a = ""; + let content_b = ""; + + if (data.history[1].file) { + content_a = data.history[1].file.text + } + + if (data.history[0].file) { + content_b = data.history[0].file.text + } + + this.setState({ + content_a: content_a, + content_b: content_b + }) + }) + } + + render() { + if (this.state.content_a !== undefined + && this.state.content_b !== undefined) { + return + } else + { + return
Loading...
+ } + } +} diff --git a/josh-ui/src/FileViewer.tsx b/josh-ui/src/FileViewer.tsx index c157e1c47..c2d99a058 100644 --- a/josh-ui/src/FileViewer.tsx +++ b/josh-ui/src/FileViewer.tsx @@ -55,7 +55,8 @@ export class FileViewer extends React.Component { } render() { - if (this.state.content !== undefined) { + //if (this.state.content !== undefined) { + if (true) { return { renderList(values: Commit[]) { - const navigate = (rev: string, e: React.MouseEvent) => { + const navigateBrowse = (rev: string, e: React.MouseEvent) => { this.props.navigateCallback(NavigateTargetType.Directory, { repo: this.props.repo, path: '', @@ -78,15 +79,31 @@ export class HistoryList extends React.Component { }) } + const navigateChange = (rev: string, e: React.MouseEvent) => { + this.props.navigateCallback(NavigateTargetType.Change, { + repo: this.props.repo, + path: '', + filter: this.props.filter, + rev: rev, + }) + } + + return values.map((entry) => { - const className = `commit-list-entry commit-list-entry-dir` - return
- {entry.hash.slice(0,8)} + return
+
{entry.summary} + {entry.authorEmail} +
+
+ Browse
+
+ }) } diff --git a/josh-ui/src/Navigation.tsx b/josh-ui/src/Navigation.tsx index 2c3eb64f9..cffdef9dd 100644 --- a/josh-ui/src/Navigation.tsx +++ b/josh-ui/src/Navigation.tsx @@ -3,7 +3,9 @@ import { gql } from 'graphql-request' export enum NavigateTargetType { History, File, - Directory + Directory, + Change, + Diff } export type NavigateTarget = { @@ -39,14 +41,46 @@ query($rev: String!, $filter: String!, $path: String!) { } ` +export const QUERY_FILE_DIFF = gql` +query($rev: String!, $filter: String!, $path: String!) { + rev(at:$rev, filter:$filter) { + history(limit: 2) { + file(path:$path) { + text + } + } + } +} +` + export const QUERY_HISTORY = gql` query($rev: String!, $filter: String!, $limit: Int) { rev(at:$rev, filter:$filter) { history(limit: $limit) { summary + authorEmail hash original: rev { hash } } } } ` + +export const QUERY_CHANGE = gql` +query($rev: String!, $filter: String!) { + rev(at:$rev, filter:$filter) { + summary + authorEmail + hash + rev { hash } + changedFiles { + from { + path + } + to { + path + } + } + } +} +`