feat(layout): redesign repo/pullsthis is pre-requesite to make it use system scheme/colors (Material You
like)
@@ -10,6 +10,7 @@ gitfoss.dev {
}
}
-www.gitfoss.dev, gitfoss.sk, gitfoss.tech {
+// www.gitfoss.dev, gitfoss.sk, gitfoss.tech {
+www.gitfoss.dev {
redir https://gitfoss.dev{uri} 301
-}
+}
@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1778263212194,
+ "_generatedAtUnix": 1778493305245,
"_hashAlgorithm": "sha1",
"_version": 2,
"assets": {
@@ -16,25 +16,25 @@
},
"islands": {
"AppRouter": {
- "hash": "8afbbf1045fc9513fc75dd93d92cb6b7fee5823e",
+ "hash": "028bbfbd3942c9040316a7b8e11f791de36c8b62",
"pathSource": "./app/islands/AppRouter.tsx",
"pathBundle": "./public/.islands/AppRouter.bundle.js",
"pathSourceMap": "./public/.islands/AppRouter.bundle.js.map"
},
"Code": {
- "hash": "2f1068f28f37b4c9fc39d082e5e443a49268bf80",
+ "hash": "4243c08b20c3b66f9613648381e0601f0003c836",
"pathSource": "./app/islands/Code.tsx",
"pathBundle": "./public/.islands/Code.bundle.js",
"pathSourceMap": "./public/.islands/Code.bundle.js.map"
},
"InstantRouterIndicator": {
- "hash": "882b1079900ca62bbed9edb917e9f98d25510389",
+ "hash": "4ba3cf445852b5aceb94ca68277e955b3bbd9e58",
"pathSource": "./app/islands/InstantRouterIndicator.tsx",
"pathBundle": "./public/.islands/InstantRouterIndicator.bundle.js",
"pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
},
"PullRequestSourceSelect": {
- "hash": "8e551193aa6fb6db829580555a4fd079c7f9f3d2",
+ "hash": "7a36e28e0dd310c98392a6de451b36cb352e017f",
"pathSource": "./app/islands/PullRequestSourceSelect.tsx",
"pathBundle": "./public/.islands/PullRequestSourceSelect.bundle.js",
"pathSourceMap": "./public/.islands/PullRequestSourceSelect.bundle.js.map"
@@ -46,13 +46,13 @@
"pathSourceMap": "./public/.islands/RepositoriesList.bundle.js.map"
},
"RepositoryCommitSummaryLine": {
- "hash": "7c3d65ecbffa640a17a60f90c9817fa3ebaa72ee",
+ "hash": "ab3729f98c645091f68deafdab42bec0ffc775c5",
"pathSource": "./app/islands/RepositoryCommitSummaryLine.tsx",
"pathBundle": "./public/.islands/RepositoryCommitSummaryLine.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryCommitSummaryLine.bundle.js.map"
},
"RepositoryCreateForm": {
- "hash": "546ac2b54b8b42e6dd1b499cdd865b650659900e",
+ "hash": "f5576f3b97071bb864350953ee55c532c11d9553",
"pathSource": "./app/islands/RepositoryCreateForm.tsx",
"pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
@@ -64,13 +64,13 @@
"pathSourceMap": "./public/.islands/RepositoryFilesDiffsList.bundle.js.map"
},
"RepositoryForkForm": {
- "hash": "e744c88ef2c2885a32b5594732b1cfab088f4350",
+ "hash": "fd08d2bea0816d5fbea2080e629631e1e1e16bd0",
"pathSource": "./app/islands/RepositoryForkForm.tsx",
"pathBundle": "./public/.islands/RepositoryForkForm.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryForkForm.bundle.js.map"
},
"RepositoryHero": {
- "hash": "86d474ab9b3342158f964569b85d3e6b9aeba9e3",
+ "hash": "b0ab4c086a536112a3d6bc3ff3c6c65d29d25288",
"pathSource": "./app/islands/RepositoryHero.tsx",
"pathBundle": "./public/.islands/RepositoryHero.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryHero.bundle.js.map"
@@ -82,19 +82,19 @@
"pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
},
"RepositoryPullRequestCreateForm": {
- "hash": "6b7fca0b6cabeacdb1cc6129535dfdb67ab0e9e0",
+ "hash": "e8c07459002da048557b8eaedbae5656f70e424f",
"pathSource": "./app/islands/RepositoryPullRequestCreateForm.tsx",
"pathBundle": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js.map"
},
"RepositoryTreeView": {
- "hash": "dc566c91c41bd9fc04da9e7017cd168f008c8b72",
+ "hash": "6774ee402778e12236e8501a4a49339f68b34edb",
"pathSource": "./app/islands/RepositoryTreeView.tsx",
"pathBundle": "./public/.islands/RepositoryTreeView.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryTreeView.bundle.js.map"
},
"SSHKeyItem": {
- "hash": "31e551858d85e502e5de1a37699de2225916ba96",
+ "hash": "ee31649cf2c982c83452e871d1e61836fef95ae5",
"pathSource": "./app/islands/SSHKeyItem.tsx",
"pathBundle": "./public/.islands/SSHKeyItem.bundle.js",
"pathSourceMap": "./public/.islands/SSHKeyItem.bundle.js.map"
@@ -106,7 +106,7 @@
"pathSource": "./app/views/HomeView.tsx"
},
"InternalErrorView": {
- "hash": "af1d1eb51c24336b7f57985d6a8039aebccd7387",
+ "hash": "aae72693c596a7f8ff05092541bb95d0ef91365a",
"pathSource": "./app/views/InternalErrorView.tsx"
},
"LoginView": {
@@ -122,23 +122,23 @@
"pathSource": "./app/views/organization/OrganizationDetailsView.tsx"
},
"RepositoryBrowserView": {
- "hash": "7b41bf07b8f2b7533023c0ddd01a254ccc04c216",
+ "hash": "81e6af41d08eeaa67205562808e6242aa9f80190",
"pathSource": "./app/views/repository/RepositoryBrowserView.tsx"
},
"RepositoryCommitsLogView": {
- "hash": "0d8c3d27a27e82f345ef5be98122927103a60d09",
+ "hash": "ff424e3c6b651f9ec41cade96ade44e5911961c5",
"pathSource": "./app/views/repository/RepositoryCommitsLogView.tsx"
},
"RepositoryCompareView": {
- "hash": "a7ac4991abcd466ff0d6862ab3cc1f3c1541f317",
+ "hash": "6a9dfc6918a092e2d5477c654da192ccc4923b11",
"pathSource": "./app/views/repository/RepositoryCompareView.tsx"
},
"RepositoryCreateView": {
- "hash": "d38e91def64064530cad123022ea856100bf79ae",
+ "hash": "9f0f39d67a7f8d4d5acc229eca4328e427e62747",
"pathSource": "./app/views/repository/RepositoryCreateView.tsx"
},
"RepositoryDetailsView": {
- "hash": "6162522f8245f1287a3b0a974278f38308203d20",
+ "hash": "1a1dae11fde756abeac809db2f7fb7417de1620f",
"pathSource": "./app/views/repository/RepositoryDetailsView.tsx"
},
"RepositoryExploreView": {
@@ -146,23 +146,23 @@
"pathSource": "./app/views/repository/RepositoryExploreView.tsx"
},
"RepositoryForkView": {
- "hash": "af5b2ddc25d33b4d48f0ac2dfc6e4e6537be7354",
+ "hash": "b8067358faf3af2ff271b14d1c710ea3b146e333",
"pathSource": "./app/views/repository/RepositoryForkView.tsx"
},
"RepositoryShowObjectView": {
- "hash": "428390b5d94c5806b809cde4bf912a663be0ca36",
+ "hash": "17463b3826771e745e6bf2234edace3482d41c68",
"pathSource": "./app/views/repository/RepositoryShowObjectView.tsx"
},
"RepositoryPullRequestCreateView": {
- "hash": "25a239fcc8d0aa05e3cc312d51b2e6f018e7cd65",
+ "hash": "0120b3883fd89467285f44cb02a11fdebd314363",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestCreateView.tsx"
},
"RepositoryPullRequestDetailsView": {
- "hash": "2fa65a3012cc05738f83656d4f0e773ebd0e5bb3",
+ "hash": "8d09c65ae26d7865c472300bf5a102470516b0f2",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx"
},
"RepositoryPullRequestsView": {
- "hash": "1816a6fb6b830daa31d3a371ef2712b3cd634188",
+ "hash": "3a3141f2902e33a4fe531d86153a14947c727dfd",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestsView.tsx"
},
"SettingsKeyAddView": {
@@ -178,7 +178,7 @@
"pathSource": "./app/views/settings/SettingsKeysListView.tsx"
},
"SettingsView": {
- "hash": "0d7e4617b9b905ab8cce136062e9e3504d9fa138",
+ "hash": "ca5ade3fa3a5c34faaa321fbdb5bc844f46ce37a",
"pathSource": "./app/views/settings/SettingsView.tsx"
},
"UserDashboardView": {
@@ -16,5 +16,5 @@ export const Card = styled.div<WithThemeSchemeProp>`
border: 1px solid ${NamedColors.BORDER_CARD[themeScheme]};
`};
- border-radius: 8px;
+ border-radius: 12px;
`;
@@ -0,0 +1,297 @@
+// 3rd-party
+import React from "react";
+import styled, { css } from "styled-components";
+// import Color from "color";
+
+// app
+import { Const } from "../const";
+import { Chip } from "./Chip";
+import { NamedColors } from "../utils/style";
+import { type CommonViewProps, type WithThemeSchemeProp } from "../types";
+import { buildRouteLink } from "../utils/shared";
+import { AppRoute } from "../routes.defs";
+
+export const DrawerPrimary = ({
+ visible = false,
+ commonProps,
+ themeScheme,
+ orgSlug,
+ repoSlug,
+ currentRef = Const.DEFAULT_HEAD_REF,
+ path = "/",
+}: WithThemeSchemeProp & {
+ visible: boolean;
+ commonProps: CommonViewProps;
+ orgSlug: string;
+ repoSlug: string;
+ currentRef?: string;
+ path?: string;
+}) => {
+ const pathRepo = buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ });
+
+ const pathRepoTrailing = buildRouteLink(
+ AppRoute.REPOSITORY_DETAILS_WITH_TRAILING_SLASH,
+ {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ },
+ );
+
+ const pathFiles = buildRouteLink(AppRoute.REPOSITORY_BROWSER, {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ currentRef: currentRef,
+ "*": path,
+ });
+
+ const pathPulls = buildRouteLink(AppRoute.REPOSITORY_PULL_REQUESTS, {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ });
+
+ if (visible === false) {
+ return null;
+ }
+
+ return (
+ <StyledDrawerPrimary themeScheme={themeScheme}>
+ <StyledDrawerHeader>
+ <StyledLogoArea themeScheme={themeScheme}>
+ <a href={"/"}>
+ <h1>{Const.APP_NAME}</h1>
+ </a>
+ </StyledLogoArea>
+ </StyledDrawerHeader>
+ <StyledDrawerContent>
+ <StyledDrawerListHeader>
+ <span>acme-org</span>
+ <span>/</span>
+ <span>my-app</span>
+ </StyledDrawerListHeader>
+ <StyledDrawerList>
+ <StyledDrawerListItem
+ themeScheme={themeScheme}
+ href={pathFiles}
+ className={
+ [pathFiles, pathRepo, pathRepoTrailing].includes(
+ commonProps.path || "/",
+ )
+ ? "active"
+ : undefined
+ }
+ >
+ <span>Files</span>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem
+ themeScheme={themeScheme}
+ href={pathPulls}
+ className={commonProps.path === pathPulls ? "active" : undefined}
+ >
+ <span>Pull Requests</span>
+ <Chip>0</Chip>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Tests & Coverage</span>
+ <Chip>0</Chip>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Builds</span>
+ <Chip>0</Chip>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Issues</span>
+ <Chip>0</Chip>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>API Reference</span>
+ <Chip>0</Chip>
+ </StyledDrawerListItem>
+ </StyledDrawerList>
+ <StyledDrawerListHeader></StyledDrawerListHeader>
+ <StyledDrawerList></StyledDrawerList>
+ </StyledDrawerContent>
+ <StyledDrawerFooter>
+ <StyledDrawerList>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Feedback</span>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Help Center</span>
+ </StyledDrawerListItem>
+ <StyledDrawerListItem themeScheme={themeScheme} disabled>
+ <span>Settings</span>
+ </StyledDrawerListItem>
+ </StyledDrawerList>
+ </StyledDrawerFooter>
+ </StyledDrawerPrimary>
+ );
+};
+
+const StyledDrawerPrimary = styled.aside<
+ WithThemeSchemeProp & { color?: string }
+>`
+ ${({ themeScheme }) => css`
+ min-width: 230px;
+ max-width: 320px;
+ height: 100vh;
+
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ justify-content: center;
+
+ position: sticky;
+ top: 0;
+ left: 0;
+ bottom: 0;
+
+ background: ${NamedColors.HEADER[themeScheme]};
+ border-right: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]};
+
+ @media only screen and (max-width: 768px) {
+ display: none;
+ }
+ `};
+`;
+
+const StyledDrawerHeader = styled.header`
+ width: 100%;
+ height: 64px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+const StyledLogoArea = styled.div<WithThemeSchemeProp>`
+ @media only screen and (max-width: 768px) {
+ & > a > h1 {
+ font-size: 22px;
+ }
+ }
+
+ & > a {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ ${({ themeScheme }) => css`
+ color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+ `};
+
+ h1 {
+ margin: 0;
+ }
+ }
+`;
+
+const StyledDrawerContent = styled.main`
+ width: 100%;
+ max-width: 100%;
+ min-width: 100%;
+ height: 100%;
+
+ flex: 1;
+
+ padding: 12px;
+`;
+
+const StyledDrawerListHeader = styled.section`
+ width: 100%;
+ height: 40px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ font-weight: bold;
+
+ margin-bottom: 4px;
+`;
+
+const StyledDrawerList = styled.section`
+ width: 100%;
+
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ gap: 2px;
+`;
+
+const StyledDrawerListItem = styled.a<
+ WithThemeSchemeProp & { disabled?: boolean }
+>`
+ ${({ disabled, themeScheme }) => css`
+ width: 100%;
+ height: 40px;
+
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ padding: 0 12px;
+
+ font-weight: normal;
+ font-size: 14px;
+ color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+ border-radius: 20px;
+ text-decoration: none;
+
+ & > span:nth-child(1) {
+ flex: 1;
+ }
+
+ & > ${Chip} {
+ color: ${NamedColors.TEXT_MUTED[themeScheme]};
+ background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
+ }
+
+ ${(disabled == null || disabled === false) &&
+ css`
+ &.active,
+ &:not(:disabled):hover {
+ color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+ background-color: ${NamedColors.CARD[themeScheme]};
+ font-weight: bold;
+ font-family: monospace;
+ }
+ `};
+
+ &:hover {
+ text-decoration: none;
+ }
+
+ &:disabled {
+ color: ${NamedColors.TEXT_MUTED[themeScheme]};
+ }
+
+ ${disabled &&
+ css`
+ color: ${NamedColors.TEXT_MUTED[themeScheme]};
+
+ & > ${Chip} {
+ color: ${NamedColors.TEXT_MUTED[themeScheme]};
+ opacity: 0.3;
+ }
+ `}
+ `}
+`;
+
+const StyledDrawerFooter = styled.footer`
+ width: 100%;
+ height: 128px;
+
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+
+ margin: 16px 0;
+ padding: 0 12px;
+`;
@@ -7,12 +7,18 @@ import { Const } from "../const";
import { NamedColors } from "../utils/style";
// app islands
import InstantRouterIndicator from "../islands/InstantRouterIndicator";
-
+// app components
import { PageHeader } from "./PageHeader";
+import { DrawerPrimary } from "./DrawerPrimary";
interface LayoutProps extends CommonViewProps {
foo?: boolean;
appVersion: string;
+ showDrawerPrimary?: boolean;
+ orgSlug?: string;
+ repoSlug?: string;
+ currentRef?: string;
+ path?: string;
}
const BRANDLINE_HEIGHT = 4;
@@ -23,7 +29,17 @@ function removeCommentsAndSpacing(str = "") {
}
export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
- const { appVersion, children, gitStamp, themeScheme } = commonProps;
+ const {
+ appVersion,
+ children,
+ gitStamp,
+ themeScheme,
+ showDrawerPrimary = false,
+ orgSlug = "",
+ repoSlug = "",
+ currentRef = Const.DEFAULT_HEAD_REF,
+ path = "",
+ } = commonProps;
const sharedProps = {
themeScheme,
@@ -78,22 +94,38 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
<div data-islandid={`${InstantRouterIndicator.name}$$0`}>
<InstantRouterIndicator />
</div>
- <StyledPageHeaderWrapper {...sharedProps}>
- <PageHeader commonProps={commonProps} themeScheme={themeScheme} />
- </StyledPageHeaderWrapper>
<StyledPageWrapper>
- <StyledChildrenWrapper {...sharedProps}>
+ <DrawerPrimary
+ commonProps={commonProps as any}
+ themeScheme={themeScheme}
+ visible={showDrawerPrimary}
+ orgSlug={orgSlug}
+ repoSlug={repoSlug}
+ currentRef={currentRef}
+ path={path}
+ />
+ <StyledChildrenWrapper
+ {...sharedProps}
+ showDrawerPrimary={showDrawerPrimary}
+ >
+ <StyledPageHeaderWrapper {...sharedProps}>
+ <PageHeader
+ commonProps={commonProps}
+ themeScheme={themeScheme}
+ forceShowLogo={showDrawerPrimary !== true}
+ />
+ </StyledPageHeaderWrapper>
{children}
+ <StyledFooterWrapper>
+ <p>
+ <a href={"https://gitfoss.dev/ethicdevs/gitfoss"}>
+ {Const.APP_NAME} • v{appVersion} (#
+ {gitStamp.slice(0, 7)}) • MIT License
+ </a>
+ </p>
+ </StyledFooterWrapper>
</StyledChildrenWrapper>
</StyledPageWrapper>
- <StyledFooterWrapper>
- <p>
- <a href={"https://gitfoss.io/ethicdevs/gitfoss"}>
- {Const.APP_NAME} - v{appVersion} (#{gitStamp.slice(0, 7)}) - MIT
- License
- </a>
- </p>
- </StyledFooterWrapper>
</StyledLayoutWrapper>
</>
);
@@ -169,7 +201,7 @@ const StyledPageWrapper = styled.div`
min-height: calc(100% - ${BRANDLINE_HEIGHT + HEADER_HEIGHT}px);
`;
-const StyledChildrenWrapper = styled.div`
+const StyledChildrenWrapper = styled.div<{ showDrawerPrimary?: boolean }>`
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
@@ -177,6 +209,16 @@ const StyledChildrenWrapper = styled.div`
flex: 1;
width: 100%;
+
+ ${({ showDrawerPrimary = false }) =>
+ showDrawerPrimary &&
+ css`
+ max-width: calc(100% - 230px);
+ `};
+
+ @media only screen and (max-width: 768px) {
+ max-width: 100%;
+ }
`;
const StyledFooterWrapper = styled.div`
@@ -9,11 +9,14 @@ import { NamedColors } from "../utils/style";
import { buildRouteLink } from "../utils/shared";
import { PageWrapper } from "./PageWrapper";
-interface PageHeaderProps extends CommonProps {}
+interface PageHeaderProps extends CommonProps {
+ forceShowLogo?: boolean;
+}
export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
commonProps,
themeScheme,
+ forceShowLogo = true,
}) => {
const invertThemeScheme = themeScheme === "light" ? "dark" : "light";
@@ -23,15 +26,19 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
<>
<a
aria-label={"View your profile and repositories"}
+ title={`View @${commonProps.currentUserUsername || "ghost"} profile and settings`}
href={buildRouteLink(
AppRoute.USER_DETAILS,
{
username: commonProps.currentUserUsername || "ghost",
},
- { encodeURIComponent: false }
+ { encodeURIComponent: false },
)}
>
- {commonProps.currentUserUsername || "ghost"}
+ <PageHeaderAvatar
+ aria-label={commonProps.currentUserUsername || "ghost"}
+ themeScheme={themeScheme}
+ />
</a>
</>
);
@@ -58,26 +65,51 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
return (
<StyledPageHeader themeScheme={themeScheme}>
<PageWrapper>
- <StyledLogoArea themeScheme={themeScheme}>
+ <StyledLogoArea themeScheme={themeScheme} forceShowLogo={forceShowLogo}>
<a href={"/"}>
- <h1 style={{ margin: 0 }}>{Const.APP_NAME}</h1>
+ <h1>{Const.APP_NAME}</h1>
</a>
</StyledLogoArea>
- <StyledPageHeaderNav>
+ <StyledPageHeaderNav themeScheme={themeScheme}>
<a
aria-label={"Explore Repositories"}
href={buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)}
+ className={
+ commonProps.path ===
+ buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)
+ ? "active"
+ : undefined
+ }
>
Explore
</a>
+ <a
+ aria-label={"Contribute to GitFOSS development"}
+ href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug: "ethicdevs",
+ repoSlug: "gitfoss",
+ })}
+ className={
+ commonProps.path ===
+ buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug: "ethicdevs",
+ repoSlug: "gitfoss",
+ })
+ ? "active"
+ : undefined
+ }
+ >
+ Contribute
+ </a>
</StyledPageHeaderNav>
+ <div style={{ flex: 1 }} />
<StyledActionsArea>
{commonProps.authenticated && (
<a
aria-label={"Create a new Repository"}
href={buildRouteLink(AppRoute.REPOSITORY_CREATE, null)}
>
- New Repository
+ (+) Repo
</a>
)}
<a
@@ -88,7 +120,7 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
})}
title={`Click to enable ${invertThemeScheme} mode`}
>
- {`${themeScheme === "light" ? "Dark" : "Light"} mode`}
+ {`${themeScheme === "light" ? "Dark" : "Light"}`}
</a>
{pageHeaderActions}
</StyledActionsArea>
@@ -106,6 +138,13 @@ const StyledPageHeader = styled.header<WithThemeSchemeProp>`
height: 100%;
width: 100%;
+ /* above mobile size */
+ @media only screen and (min-width: 768px) {
+ & > ${PageWrapper} {
+ padding: 0 16px;
+ }
+ }
+
& > ${PageWrapper} {
height: 100%;
@@ -113,7 +152,7 @@ const StyledPageHeader = styled.header<WithThemeSchemeProp>`
justify-content: flex-start;
align-items: center;
- padding: 0 16px;
+ padding: 0;
gap: 16px;
}
@@ -134,7 +173,23 @@ const StyledPageHeader = styled.header<WithThemeSchemeProp>`
}
`;
-const StyledLogoArea = styled.div<WithThemeSchemeProp>`
+const StyledLogoArea = styled.div<
+ WithThemeSchemeProp & { forceShowLogo: boolean }
+>`
+ ${({ forceShowLogo }) =>
+ forceShowLogo !== true &&
+ css`
+ @media only screen and (min-width: 768px) {
+ display: none;
+ }
+ `};
+
+ @media only screen and (max-width: 768px) {
+ & > a > h1 {
+ font-size: 22px;
+ }
+ }
+
& > a {
display: flex;
flex-flow: row nowrap;
@@ -144,24 +199,64 @@ const StyledLogoArea = styled.div<WithThemeSchemeProp>`
${({ themeScheme }) => css`
color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
`};
+
+ h1 {
+ margin: 0;
+ }
}
`;
-const StyledPageHeaderNav = styled.nav`
+const StyledPageHeaderNav = styled.nav<WithThemeSchemeProp>`
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: center;
- flex: 1;
- height: 100%;
- width: 100%;
+ /* flex: 1; */
+ height: 40px;
+ /* width: 100%; */
+
+ gap: 2px;
+ margin: 0;
- gap: 24px;
- margin-left: 4px;
+ ${({ themeScheme }) => css`
+ border-radius: 20px;
+ background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
+ `};
+
+ @media only screen and (max-width: 768px) {
+ display: none;
+ }
& > a {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
white-space: nowrap;
+
+ height: 40px;
+ padding: 0 16px;
+
+ border-radius: 20px;
+ font-weight: normal;
+ text-decoration: none;
+
+ ${({ themeScheme }) => css`
+ color: ${NamedColors.TEXT_MUTED[themeScheme]};
+ /* background-color: ${NamedColors.CARD[themeScheme]}; */
+ `};
+
+ &.active,
+ &:hover {
+ ${({ themeScheme }) => css`
+ color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+ background-color: ${NamedColors.CARD[themeScheme]};
+ font-weight: bold;
+ font-family: monospace;
+ text-decoration: none;
+ `};
+ }
}
`;
@@ -176,3 +271,16 @@ const StyledActionsArea = styled.div`
white-space: nowrap;
}
`;
+
+const PageHeaderAvatar = styled.img<WithThemeSchemeProp>`
+ width: 40px;
+ height: 40px;
+
+ border-image: none;
+ border-radius: 40px;
+
+ ${({ themeScheme }) => css`
+ border: 1px solid ${NamedColors.BORDER_CARD[themeScheme]};
+ background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
+ `};
+`;
@@ -53,7 +53,7 @@ const RepositoryHero: ReactIsland<RepositoryHeroProps> = ({
</a>
{` ${separator} `}
{path == null ? (
- <span style={{ textTransform: "capitalize" }}>
+ <span style={{ textTransform: "capitalize", fontSize: 16 }}>
({repo.visibility.toLowerCase()})
</span>
) : (
@@ -37,6 +37,7 @@ export interface CommonViewProps {
flashMessage: string | null;
themeScheme: AppThemeScheme;
title?: string;
+ path?: string;
}
export type CommonProps = { commonProps: CommonViewProps };
@@ -49,6 +49,7 @@ export const makeRequestHandler = {
gitStamp: request.gitStamp,
themeScheme,
title,
+ path: request.url,
},
} as T & { commonProps: CommonViewProps };
@@ -15,10 +15,10 @@ export interface InternalErrorViewProps extends CommonProps {
error: FastifyError;
}
-const DEV = process.env.NODE_ENV === "development";
-const DEBUG = !!(
- process.env.DEBUG != null && ["true", "1", true].includes(process.env.DEBUG)
-);
+// const DEV = process.env.NODE_ENV === "development";
+// const DEBUG = !!(
+// process.env.DEBUG != null && ["true", "1", true].includes(process.env.DEBUG)
+// );
const InternalErrorView: ReactView<InternalErrorViewProps> = ({
commonProps,
@@ -55,8 +55,7 @@ const InternalErrorView: ReactView<InternalErrorViewProps> = ({
)}
{isInternalError && (
<h1>
- 😵💫 Woops... we've encountered an internal error, please
- apologize.
+ 😵💫 Woops... we've encountered an internal error, please apologize.
</h1>
)}
<div style={{ marginTop: 8 }}>
@@ -69,66 +68,66 @@ const InternalErrorView: ReactView<InternalErrorViewProps> = ({
<p>Sorry but it is not possible to recover from this error.</p>
))}
</div>
- {(DEBUG || DEV) && (
- <div style={{ marginTop: 24 }}>
- {getThemedCodeCss(commonProps.themeScheme)}
- <details open>
- <summary>[DEBUG] Full error details:</summary>
- {message != null && message.trim() !== "" && (
- <div style={{ marginTop: 16 }}>
- <label
- style={{ fontWeight: "bold", textDecoration: "underline" }}
- >
- Message:
- </label>
- <pre>
- <code>{message.trim()}</code>
- </pre>
- </div>
- )}
- {stack != null && stack.trim() !== "" && (
- <div style={{ marginTop: 16 }}>
- <label
- style={{ fontWeight: "bold", textDecoration: "underline" }}
- >
- Stack:
- </label>
- <Card
- data-islandid={`${Code.name}$$0`}
- style={{ width: "100%", marginTop: 32 }}
+ {/* {(DEBUG || DEV) && ( */}
+ <div style={{ maxWidth: "100%", marginTop: 24 }}>
+ {getThemedCodeCss(commonProps.themeScheme)}
+ <details open>
+ <summary>[DEBUG] Full error details:</summary>
+ {message != null && message.trim() !== "" && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Message:
+ </label>
+ <pre>
+ <code style={{whiteSpace:'pre-wrap'}}>{message.trim()}</code>
+ </pre>
+ </div>
+ )}
+ {stack != null && stack.trim() !== "" && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Stack:
+ </label>
+ <Card
+ data-islandid={`${Code.name}$$0`}
+ style={{ width: "100%", marginTop: 32 }}
+ themeScheme={commonProps.themeScheme}
+ >
+ <Code
+ language={"python"}
+ code={stack.replace(message, "").trim()}
themeScheme={commonProps.themeScheme}
- >
- <Code
- language={"python"}
- code={stack.replace(message, "").trim()}
- themeScheme={commonProps.themeScheme}
- />
- </Card>
- </div>
- )}
- {validation != null && (
- <div style={{ marginTop: 16 }}>
- <label
- style={{ fontWeight: "bold", textDecoration: "underline" }}
- >
- Validation:
- </label>
- <Card
- data-islandid={`${Code.name}$$1`}
- style={{ width: "100%", marginTop: 32 }}
+ />
+ </Card>
+ </div>
+ )}
+ {validation != null && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Validation:
+ </label>
+ <Card
+ data-islandid={`${Code.name}$$1`}
+ style={{ width: "100%", marginTop: 32 }}
+ themeScheme={commonProps.themeScheme}
+ >
+ <Code
+ language={"json"}
+ code={JSON.stringify(validation, null, 2)}
themeScheme={commonProps.themeScheme}
- >
- <Code
- language={"json"}
- code={JSON.stringify(validation, null, 2)}
- themeScheme={commonProps.themeScheme}
- />
- </Card>
- </div>
- )}
- </details>
- </div>
- )}
+ />
+ </Card>
+ </div>
+ )}
+ </details>
+ </div>
+ {/* )} */}
</PageWrapper>
</Layout>
);
@@ -56,10 +56,14 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
const currPathParts = path.split("/");
const shouldShowRootPath = path !== "/";
- console.log("currentRef:", currentRef);
-
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ currentRef={currentRef}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -36,7 +36,13 @@ const RepositoryCommitsLogView: ReactView<RepositoryCommitsLogViewProps> = ({
repo,
}) => {
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ currentRef={currentRef}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -33,7 +33,13 @@ const RepositoryCompareView: ReactView<RepositoryCompareViewProps> = ({
refB,
}) => {
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ currentRef={refA}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -66,7 +66,13 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
}) => {
const { forkedFromRepo, ...repo } = repoWithForkedFromRepoMetas;
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ currentRef={currentRef}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -220,7 +226,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
<span>{keyword}</span>
{idx === self.length - 1 ? "." : ", "}
</React.Fragment>
- )
+ ),
)}
</p>
)}
@@ -273,14 +279,14 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
path !== "/"
? path
: "",
- }
+ },
)}
>
{branch}
</a>
{idx === self.length - 1 ? "." : ", "}
</React.Fragment>
- )
+ ),
)}
</p>
)}
@@ -305,7 +311,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
<span>{tag}</span>
{idx === self.length - 1 ? "." : ", "}
</React.Fragment>
- )
+ ),
)}
</p>
)}
@@ -35,7 +35,12 @@ const RepositoryForkView: ReactView<RepositoryForkViewProps> = ({
initialValues = undefined,
}) => {
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={sourceParentOrg.slug}
+ repoSlug={sourceRepo.slug}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -43,15 +43,21 @@ const RepositoryShowObjectView: ReactView<RepositoryShowObjectViewProps> = ({
}) => {
const totalAdditions = gitObjectDiffs?.reduce(
(acc, obj) => (acc += obj.additions),
- 0
+ 0,
);
const totalDeletions = gitObjectDiffs?.reduce(
(acc, obj) => (acc += obj.deletions),
- 0
+ 0,
);
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ currentRef={currentRef}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -21,35 +21,41 @@ export interface RepositoryPullRequestCreateViewProps extends CommonProps {
variant: RepositoryPullRequestCreateFormVariant;
}
-const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateViewProps> =
- ({ commonProps, parentOrg, repo, variant }) => {
- return (
- <Layout {...commonProps}>
- <PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
- <RepositoryHero
- forkedFromRepo={repo.forkedFromRepo}
- forksCount={repo.forks.length}
- parentOrg={parentOrg}
- path={`Pull Requests / New`}
- repo={repo}
- />
- </IslandWrapper>
- <IslandWrapper
- data-islandid={`${RepositoryPullRequestCreateForm.name}$$0`}
- style={{ marginTop: 24 }}
- >
- <RepositoryPullRequestCreateForm
- parentOrgSlug={parentOrg.slug}
- repoSlug={repo.slug}
- themeScheme={commonProps.themeScheme}
- variant={variant}
- />
- </IslandWrapper>
- </PageWrapper>
- </Layout>
- );
- };
+const RepositoryPullRequestCreateView: ReactView<
+ RepositoryPullRequestCreateViewProps
+> = ({ commonProps, parentOrg, repo, variant }) => {
+ return (
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ >
+ <PageWrapper>
+ <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <RepositoryHero
+ forkedFromRepo={repo.forkedFromRepo}
+ forksCount={repo.forks.length}
+ parentOrg={parentOrg}
+ path={`Pull Requests / New`}
+ repo={repo}
+ />
+ </IslandWrapper>
+ <IslandWrapper
+ data-islandid={`${RepositoryPullRequestCreateForm.name}$$0`}
+ style={{ marginTop: 24 }}
+ >
+ <RepositoryPullRequestCreateForm
+ parentOrgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ themeScheme={commonProps.themeScheme}
+ variant={variant}
+ />
+ </IslandWrapper>
+ </PageWrapper>
+ </Layout>
+ );
+};
RepositoryPullRequestCreateView.displayName = "RepositoryPullRequestCreateView";
export default RepositoryPullRequestCreateView;
@@ -68,7 +68,7 @@ const RepositoryPullRequestDetailsView: ReactView<
};
return acc;
},
- { additions: 0, deletions: 0 }
+ { additions: 0, deletions: 0 },
);
if (pr.state !== PullRequestState.OPEN) {
@@ -76,7 +76,12 @@ const RepositoryPullRequestDetailsView: ReactView<
}
return (
- <Layout {...commonProps}>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ >
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
<RepositoryHero
@@ -110,7 +115,7 @@ const RepositoryPullRequestDetailsView: ReactView<
{
username: prAuthor.username,
},
- { encodeURIComponent: false }
+ { encodeURIComponent: false },
)}
>
{prAuthor.displayName || prAuthor.username}
@@ -124,7 +129,7 @@ const RepositoryPullRequestDetailsView: ReactView<
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
pullUid: pr.uid,
- }
+ },
)}
>
Edit PR
@@ -136,7 +141,7 @@ const RepositoryPullRequestDetailsView: ReactView<
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
pullUid: pr.uid,
- }
+ },
)}
>
Delete PR
@@ -208,7 +213,7 @@ const RepositoryPullRequestDetailsView: ReactView<
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
pullUid: pr.uid,
- }
+ },
)}
>
<Grid.Col fluid nowrap gap={8}>
@@ -21,163 +21,169 @@ export interface RepositoryPullRequestsViewProps extends CommonProps {
pullRequestsFilter?: PullRequestsFilter;
}
-const RepositoryPullRequestsView: ReactView<RepositoryPullRequestsViewProps> =
- ({
- commonProps,
- parentOrg,
- pullRequests,
- repo,
- pullRequestsFilter = "all",
- }) => {
- return (
- <Layout {...commonProps}>
- <PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
- <RepositoryHero
- forkedFromRepo={repo.forkedFromRepo}
- forksCount={repo.forks.length}
- parentOrg={parentOrg}
- path={`Pull Requests`}
- repo={repo}
- />
- </IslandWrapper>
- <Grid.Col fluid style={{ marginTop: 32 }}>
+const RepositoryPullRequestsView: ReactView<
+ RepositoryPullRequestsViewProps
+> = ({
+ commonProps,
+ parentOrg,
+ pullRequests,
+ repo,
+ pullRequestsFilter = "all",
+}) => {
+ return (
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={parentOrg.slug}
+ repoSlug={repo.slug}
+ >
+ <PageWrapper>
+ <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <RepositoryHero
+ forkedFromRepo={repo.forkedFromRepo}
+ forksCount={repo.forks.length}
+ parentOrg={parentOrg}
+ path={`Pull Requests`}
+ repo={repo}
+ />
+ </IslandWrapper>
+ <Grid.Col fluid style={{ marginTop: 32 }}>
+ <a
+ href={buildRouteLink(AppRoute.REPOSITORY_PULL_REQUEST_CREATE, {
+ orgSlug: parentOrg.slug,
+ repoSlug: repo.slug,
+ })}
+ >
+ New Pull Request
+ </a>
+ </Grid.Col>
+ <Grid.Col fluid style={{ marginTop: 24 }}>
+ <Grid.Row fluid alignItems={"center"} gap={16}>
<a
- href={buildRouteLink(AppRoute.REPOSITORY_PULL_REQUEST_CREATE, {
- orgSlug: parentOrg.slug,
- repoSlug: repo.slug,
- })}
+ style={{
+ textDecoration:
+ pullRequestsFilter === "all" ? "underline" : "none",
+ }}
+ href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
+ "all" as PullRequestsFilter
+ }`}
>
- New Pull Request
+ All
</a>
- </Grid.Col>
- <Grid.Col fluid style={{ marginTop: 24 }}>
- <Grid.Row fluid alignItems={"center"} gap={16}>
- <a
- style={{
- textDecoration:
- pullRequestsFilter === "all" ? "underline" : "none",
- }}
- href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
- "all" as PullRequestsFilter
- }`}
- >
- All
- </a>
- <a
- style={{
- textDecoration:
- pullRequestsFilter === "opened" ? "underline" : "none",
- }}
- href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
- "opened" as PullRequestsFilter
- }`}
- >
- Opened
- </a>
- <a
- style={{
- textDecoration:
- pullRequestsFilter === "merged" ? "underline" : "none",
- }}
- href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
- "merged" as PullRequestsFilter
- }`}
- >
- Merged
- </a>
- <a
- style={{
- textDecoration:
- pullRequestsFilter === "closed" ? "underline" : "none",
- }}
- href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
- "closed" as PullRequestsFilter
- }`}
+ <a
+ style={{
+ textDecoration:
+ pullRequestsFilter === "opened" ? "underline" : "none",
+ }}
+ href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
+ "opened" as PullRequestsFilter
+ }`}
+ >
+ Opened
+ </a>
+ <a
+ style={{
+ textDecoration:
+ pullRequestsFilter === "merged" ? "underline" : "none",
+ }}
+ href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
+ "merged" as PullRequestsFilter
+ }`}
+ >
+ Merged
+ </a>
+ <a
+ style={{
+ textDecoration:
+ pullRequestsFilter === "closed" ? "underline" : "none",
+ }}
+ href={`/${parentOrg.slug}/${repo.slug}/pulls?filter=${
+ "closed" as PullRequestsFilter
+ }`}
+ >
+ Closed
+ </a>
+ </Grid.Row>
+ </Grid.Col>
+ <Grid.Col fluid style={{ marginTop: 32 }}>
+ {pullRequests != null && pullRequests.length >= 1 ? (
+ pullRequests.map((pr, idx) => (
+ <Grid.Col
+ key={pr.id}
+ fluid
+ style={{ marginTop: idx === 0 ? 0 : 16 }}
>
- Closed
- </a>
- </Grid.Row>
- </Grid.Col>
- <Grid.Col fluid style={{ marginTop: 32 }}>
- {pullRequests != null && pullRequests.length >= 1 ? (
- pullRequests.map((pr, idx) => (
- <Grid.Col
- key={pr.id}
+ <a
+ href={buildRouteLink(
+ AppRoute.REPOSITORY_PULL_REQUEST_DETAILS,
+ {
+ orgSlug: parentOrg.slug,
+ repoSlug: repo.slug,
+ pullUid: pr.uid,
+ },
+ )}
+ >
+ #{pr.uid} - {pr.summary} [{pr.state}]
+ </a>
+ <span style={{ opacity: 0.67 }}>
+ wants to merge <code>{pr.sourceBranch}</code> into{" "}
+ <code>{pr.targetBranch}</code>
+ </span>
+ <Grid.Row
fluid
- style={{ marginTop: idx === 0 ? 0 : 16 }}
+ alignItems={"center"}
+ style={{ opacity: 0.67, marginTop: 4 }}
+ >
+ {new Date(pr.createdAt).getTime() <=
+ new Date(pr.updatedAt).getTime() && (
+ <span>
+ opened on {new Date(pr.createdAt).toLocaleString()}
+ </span>
+ )}
+ {((pr.closedAt == null &&
+ new Date(pr.updatedAt).getTime() >
+ new Date(pr.createdAt).getTime()) ||
+ (pr.closedAt != null &&
+ new Date(pr.updatedAt).getTime() <
+ new Date(pr.closedAt).getTime())) && (
+ <span>
+ updated on {new Date(pr.updatedAt).toLocaleString()}
+ </span>
+ )}
+ {pr.closedAt != null && (
+ <span>
+ closed on
+ {new Date(pr.closedAt).toLocaleString()}
+ </span>
+ )}
+ </Grid.Row>
+ </Grid.Col>
+ ))
+ ) : (
+ <div>
+ <h1>No Pull Request Yet</h1>
+ <p>
+ <span>Be the change you want to see, </span>
+ <a
+ href={buildRouteLink(
+ AppRoute.REPOSITORY_PULL_REQUEST_CREATE,
+ {
+ orgSlug: parentOrg.slug,
+ repoSlug: repo.slug,
+ },
+ )}
>
- <a
- href={buildRouteLink(
- AppRoute.REPOSITORY_PULL_REQUEST_DETAILS,
- {
- orgSlug: parentOrg.slug,
- repoSlug: repo.slug,
- pullUid: pr.uid,
- }
- )}
- >
- #{pr.uid} - {pr.summary} [{pr.state}]
- </a>
- <span style={{ opacity: 0.67 }}>
- wants to merge <code>{pr.sourceBranch}</code> into{" "}
- <code>{pr.targetBranch}</code>
- </span>
- <Grid.Row
- fluid
- alignItems={"center"}
- style={{ opacity: 0.67, marginTop: 4 }}
- >
- {new Date(pr.createdAt).getTime() <=
- new Date(pr.updatedAt).getTime() && (
- <span>
- opened on {new Date(pr.createdAt).toLocaleString()}
- </span>
- )}
- {((pr.closedAt == null &&
- new Date(pr.updatedAt).getTime() >
- new Date(pr.createdAt).getTime()) ||
- (pr.closedAt != null &&
- new Date(pr.updatedAt).getTime() <
- new Date(pr.closedAt).getTime())) && (
- <span>
- updated on {new Date(pr.updatedAt).toLocaleString()}
- </span>
- )}
- {pr.closedAt != null && (
- <span>
- closed on
- {new Date(pr.closedAt).toLocaleString()}
- </span>
- )}
- </Grid.Row>
- </Grid.Col>
- ))
- ) : (
- <div>
- <h1>No Pull Request Yet</h1>
- <p>
- <span>Be the change you want to see, </span>
- <a
- href={buildRouteLink(
- AppRoute.REPOSITORY_PULL_REQUEST_CREATE,
- {
- orgSlug: parentOrg.slug,
- repoSlug: repo.slug,
- }
- )}
- >
- open the first Pull Request
- </a>
- <span> to this repository 🚀.</span>
- </p>
- </div>
- )}
- </Grid.Col>
- </PageWrapper>
- </Layout>
- );
- };
+ open the first Pull Request
+ </a>
+ <span> to this repository 🚀.</span>
+ </p>
+ </div>
+ )}
+ </Grid.Col>
+ </PageWrapper>
+ </Layout>
+ );
+};
RepositoryPullRequestsView.displayName = "RepositoryPullRequestsView";
export default RepositoryPullRequestsView;
@@ -1,38 +1,36 @@
-declare module "./http_client" {
- import { IncomingMessage } from "http";
+import { IncomingMessage } from "http";
- export class HttpResponse {
- readonly statusCode: number;
- readonly statusText: string;
- readonly ok: boolean;
- readonly headers: IncomingMessage["headers"];
+export class HttpResponse {
+ readonly statusCode: number;
+ readonly statusText: string;
+ readonly ok: boolean;
+ readonly headers: IncomingMessage["headers"];
- constructor(incoming: IncomingMessage);
+ constructor(incoming: IncomingMessage);
- text(): Promise<string>;
- isJson(): Promise<boolean>;
- json(): Promise<any>;
- }
+ text(): Promise<string>;
+ isJson(): Promise<boolean>;
+ json<T extends any = any>(): Promise<T>;
+}
- export interface RequestConfig {
- headers?: Record<string, string>;
- body?: string | Buffer | any;
- }
+export interface RequestConfig {
+ headers?: Record<string, string>;
+ body?: string | Buffer | any;
+}
- export class HttpClient {
- constructor();
+export class HttpClient {
+ constructor();
- get(url: string, config?: RequestConfig): Promise<HttpResponse>;
- post(url: string, config?: RequestConfig): Promise<HttpResponse>;
- put(url: string, config?: RequestConfig): Promise<HttpResponse>;
- patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
- delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
- head(url: string, config?: RequestConfig): Promise<HttpResponse>;
- options(url: string, config?: RequestConfig): Promise<HttpResponse>;
- custom(
- method: string,
- url: string,
- config?: RequestConfig,
- ): Promise<HttpResponse>;
- }
+ get(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ post(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ put(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ head(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ options(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ custom(
+ method: string,
+ url: string,
+ config?: RequestConfig,
+ ): Promise<HttpResponse>;
}
@@ -1,39 +1,85 @@
#!/bin/sh
-SSH_ORIGINAL_COMMAND=${SSH_ORIGINAL_COMMAND}
+# Passed in the environment by ssh force_command:
+# environment="KEY_ID=...",command="/usr/bin/ssh_command ..."
+# KEY_ID="..." /usr/bin/ssh_command ...
+KEY_ID=${KEY_ID:-unset}
+
+if [ -z ${KEY_ID} ] 2>/dev/null ||
+ [ -n ${KEY_ID} ] 2>/dev/null ||
+ [ "${KEY_ID}" = "unset" ] 2>/dev/null; then
+ printf '%s\n' "Could not authorize command. KEY_ID is not set/empty."
+ exit 128
+fi
+
+# Passed as first argument by ssh force_command:
+# /usr/bin/ssh_command $1
USERNAME=$1
+if [ -z ${USERNAME} ] 2>/dev/null ||
+ [ -n ${USERNAME} ] 2>/dev/null ||
+ [ "${USERNAME}" = "unset" ] 2>/dev/null; then
+ printf '%s\n' "Could not authorize command. KEY_ID is not set/empty."
+ exit 128
+fi
+
+SSH_ORIGINAL_COMMAND=${SSH_ORIGINAL_COMMAND:-unset}
+
# If SSH_ORIGINAL_COMMAND is unset, or empty, this was not invoked by ssh ForceCommand, kill now.
# If USERNAME is unset, this was not invoked by ssh ForceCommand, kill now.
if [ -z ${SSH_ORIGINAL_COMMAND+x} ] 2>/dev/null ||
[ -n ${SSH_ORIGINAL_COMMAND} ] 2>/dev/null ||
- [ -z ${USERNAME+x} ] 2>/dev/null; then
+ [ "${SSH_ORIGINAL_COMMAND}" = "unset" ] 2>/dev/null ; then
printf '%s\n' "Hi $USER! You've successfully authenticated, but I do not"
printf '%s\n' "provide interactive shell access."
exit 128
fi
-USERNAME=$1
-RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
+RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}" "${KEY_ID}" "${SSH_ORIGINAL_COMMAND}")
EXIT=$?
-echo "===> ${RES_JSON}\n" >> /opt/ssh_commands.log
+# That's all we need to log;
+echo <<-EOF
+[git_ssh.connection.command]:
+⋗ time: $(TZ="Europe/Paris" date)
+⋗ user: ${USERNAME} (key: ${KEY_ID})
+⋗ command (original): ${SSH_ORIGINAL_COMMAND}
+EOF >> /opt/ssh_commands.log
+
+if [ "${EXIT}" != "0"]; then
+ printf '%s\n' "ssh_command_node exited with failure."
+ exit $EXIT
+fi
-echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
+# {
+# COMMAND=$(echo "$RES_JSON" | jq -r '.command')
+# AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
+# GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
+# } || {
+COMMAND=${SSH_ORIGINAL_COMMAND}
+AUTH_MODE="always"
+GIT_REPO_DIR="unset"
+# }
-COMMAND=$(echo "$RES_JSON" | jq -r '.command')
-AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
-GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
+echo <<-EOF
+⋗ command (parsed): ${SSH_ORIGINAL_COMMAND}
+⋗ auth mode: ${AUTH_MODE}
+⋗ repo path: ${GIT_REPO_DIR}
+EOF >> /opt/ssh_commands.log
-echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
+# echo <<-EOF
+# ⋗ ssh key fingerprint: 11bca03df28f0a2f95a8a11
+# ⋗ gitfoss key fingerprint: 11bca03df28f0a2f95a8a11
+# ⋗ match?: YES | NO
+# EOF >> /opt/ssh_commands.log
+# auth passed, execute git command (safe)
if [ "$EXIT" = "0" ]; then
- LANG=C $COMMAND $GIT_REPO_DIR;
+ echo "⋗ authorized?: YES (Call original command)\n\n" >> /opt/ssh_commands.log
+ COMMAND_OUTPUT=$(LANG=C $COMMAND $GIT_REPO_DIR);
exit $?
else
+ echo "⋗ authorized?: NO (Forbidden access)\n\n" >> /opt/ssh_commands.log
echo "Forbidden access.\n"
exit 1
fi
@@ -1,144 +1,116 @@
#!/usr/bin/node
-
-const { HttpClient } = require("./http_client");
-// const { HttpClient } = require("/home/debian/http_client.js");
const fs = require("fs");
-
-const LOGS_FILE = "/opt/ssh_commands.log";
-
-function log(message, ...args) {
- try {
- fs.appendFileSync(
- LOGS_FILE,
- JSON.stringify({
- timestamp: new Date().toISOString(),
- message,
- args,
- }) + "\n",
- { encoding: "utf8" },
- );
- } catch (err) {
- // console.log(message, ...args);
- }
-}
+const { HttpClient } = require("./http_client");
async function main(args, sshOriginalCommand) {
const [_, __, username] = args;
if (username == null || username.trim() === "") {
+ console.log(JSON.stringify({ success: false }));
process.exit(128);
}
if (sshOriginalCommand == null) {
+ console.log(JSON.stringify({ success: false }));
process.exit(128);
}
- const authorizedKeysBuffer = fs.readFileSync(
- "/home/git/.ssh/authorized_keys",
- { encoding: "utf8" },
- );
-
- const authKeys = authorizedKeysBuffer
- .split("\n")
- .map((line) =>
- line.startsWith("#")
- ? { type: "comment", text: line }
- : line.trim() !== ""
- ? { type: "key", text: line.trim() }
- : null,
- )
- .filter((x) => x != null && x.type === "key");
-
- // console.log("authkeys:", authKeys);
- // console.log("username", username);
-
- let userPk = authKeys.find(
- (key) =>
- key.text.includes(`command="ssh_command ${username}"`) ||
- key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
- );
-
- if (userPk == null) {
- log("No key matched ssh connection in authorized_keys file.", { username });
- return process.exit(128);
- }
+ try {
+ const authorizedKeysBuffer = fs.readFileSync(
+ "/home/git/.ssh/authorized_keys",
+ { encoding: "utf8" },
+ );
- const pk = userPk.text;
-
- const sshRsaIndex = pk.indexOf("ssh-rsa");
- const publicKey = pk.substring(sshRsaIndex);
-
- const [command, repoSlug] = sshOriginalCommand
- .split(" ")
- .map((part) => part.replace(/\'/g, "").trim());
-
- const data = JSON.stringify({
- command,
- repoSlug,
- username,
- publicKey,
- });
-
- log("Will authenticate through /_ssh/auth", {
- command,
- repoSlug,
- username,
- publicKey,
- });
-
- const client = new HttpClient();
-
- // console.log("HttpClient.instance:", client);
-
- const res = await client.post("http://localhost:1337/_ssh/auth", {
- headers: {
- "Content-Type": "application/json",
- "Content-Length": Buffer.byteLength(data),
- },
- body: data,
- });
-
- // console.log("res:", res);
-
- if (res.ok === false) {
- log(
- "/_ssh/auth response is an error:",
- res.statusCode,
- res.statusText,
- await res.text(),
+ const authKeys = authorizedKeysBuffer
+ .split("\n")
+ .map((line) =>
+ line.startsWith("#")
+ ? { type: "comment", text: line }
+ : line.trim() !== ""
+ ? { type: "key", text: line.trim() }
+ : null,
+ )
+ .filter((x) => x != null && x.type === "key");
+
+ const userPk = authKeys.find(
+ (key) =>
+ key.text.includes(`command="ssh_command ${username}"`) ||
+ key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
);
- return process.exit(128);
- }
- if ((await res.isJson()) === false) {
- log(
- "/_ssh/auth response is not json:",
- res.statusCode,
- res.statusText,
- await res.text(),
+ if (userPk == null) {
+ console.log(JSON.stringify({ success: false }));
+ return process.exit(128);
+ }
+
+ const pk = userPk.text;
+
+ const sshRsaIndex = pk.indexOf("ssh-rsa");
+ const publicKey = pk.substring(sshRsaIndex);
+
+ const [command, repoSlug] = sshOriginalCommand
+ .split(" ")
+ .map((part) => part.replace(/\'/g, "").trim());
+
+ const client = new HttpClient();
+
+ const data = JSON.stringify({
+ command,
+ repoSlug,
+ username,
+ publicKey,
+ });
+
+ // authenticate against live prod api by default.
+ const DEPLOYMENT_DOMAIN = process.env.DEPLOYMENT_DOMAIN || "127.0.0.1"; //|| "gitfoss.dev";
+ const DEPLOYMENT_SCHEME = process.env.DEPLOYMENT_SCHEME || "http"; //|| "https";
+ const PORT = process.env.PORT || 1337; //|| 443;
+ const PORT_STR = DEPLOYMENT_SCHEME === "https" ? "" : `:${PORT}`;
+ const SSH_AUTH_HELPER_URL = `${DEPLOYMENT_SCHEME}://${DEPLOYMENT_DOMAIN}${PORT_STR}/_ssh/auth`;
+
+ const res = await client.post(SSH_AUTH_HELPER_URL, {
+ headers: {
+ "Content-Type": "application/json",
+ "Content-Length": Buffer.byteLength(data),
+ },
+ body: data,
+ });
+
+ if (res.ok === false) {
+ console.log(JSON.stringify({ success: false }));
+ return process.exit(128);
+ }
+
+ if ((await res.isJson()) === false) {
+ console.log(JSON.stringify({ success: false }));
+ return process.exit(128);
+ }
+
+ const json = await res.json();
+
+ if (json.success === false) {
+ console.log(JSON.stringify({ success: false }));
+ return process.exit(128);
+ }
+ } catch (e) {
+ console.log(
+ JSON.stringify({
+ success: false,
+ error: e,
+ }),
);
- return process.exit(128);
+ process.exit(128);
}
- const json = await res.json();
+ // success!
+ // print only the json so ssh_command can parse and continue
- log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
+ const GIT_REPOSITORIES_ROOT =
+ process.env.GIT_REPOSITORIES_ROOT || "/var/lib/gitfoss/repos";
- if (json.success === false) {
- log(
- "/_ssh/auth response is not successful:",
- res.statusCode,
- res.statusText,
- json,
- );
- return process.exit(128);
- }
+ json.gitRepositoryDir = `${GIT_REPOSITORIES_ROOT}${repoSlug.replace(/\.git$/, "")}.git`;
- json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
- console.log(JSON.stringify(json));
-
- // success!
- log("/_ssh/auth response success!", json);
+ console.log(JSON.stringify({ success: true, ...json }));
process.exit(0);
}
@@ -15,31 +15,31 @@ services:
- 5432:5432
volumes:
- ./data/postgres_data:/var/lib/postgresql/data
- web:
- container_name: gitfoss_web
- build:
- context: .
- dockerfile: Dockerfile
- args:
- - HOST=0.0.0.0
- - PORT=1337
- depends_on:
- - db
- ports:
- - 1337:1337
- - 22:22
- volumes:
- - ./data/gitfoss_repos:/var/lib/gitfoss/repos
- - ./data/gitfoss_repos:/home/git/repos
- - ./data/authorized_keys:/home/git/.ssh/authorized_keys
- env_file: .env.docker
- # environment:
- # - COOKIE_NAME=gitfoss_ssid
- # - COOKIE_SECRET=gitfoss-cookie-secret
- # - DATABASE_URL=postgresql://postgres:postgres@gitfoss_db:5432/gitfoss_local?sslmode=disable&connection_limit=3
- # - DEPLOYMENT_DOMAIN=local-app.localhost
- # - DEPLOYMENT_SCHEME=http
- # - GIT_REPOSITORIES_ROOT=/var/lib/gitfoss/repos
+ # web:
+ # container_name: gitfoss_web
+ # build:
+ # context: .
+ # dockerfile: Dockerfile
+ # args:
+ # - HOST=0.0.0.0
+ # - PORT=1337
+ # depends_on:
+ # - db
+ # ports:
+ # - 1337:1337
+ # - 22:22
+ # volumes:
+ # - ./data/gitfoss_repos:/var/lib/gitfoss/repos
+ # - ./data/gitfoss_repos:/home/git/repos
+ # - ./data/authorized_keys:/home/git/.ssh/authorized_keys
+ # env_file: .env.docker
+ # environment:
+ # - COOKIE_NAME=gitfoss_ssid
+ # - COOKIE_SECRET=gitfoss-cookie-secret
+ # - DATABASE_URL=postgresql://postgres:postgres@gitfoss_db:5432/gitfoss_local?sslmode=disable&connection_limit=3
+ # - DEPLOYMENT_DOMAIN=local-app.localhost
+ # - DEPLOYMENT_SCHEME=http
+ # - GIT_REPOSITORIES_ROOT=/var/lib/gitfoss/repos
# git_ssh:
# container_name: gitfoss_git_ssh
# build:
@@ -1,6 +1,7 @@
todo:
-- [x] make ssh server work every times !!!!!
+- [x] make ssh server work
+- [ ] make ssh server work every times !!!!!
- [x] make the islands runtime load dependencies properly
- [ ] finish merge pull request feature
- [x] add ssh key feature