Initial skeleton for Repository Resource View flow (#30)

* API integration for Repos listing page

* Add custom branch when creating a repo

* Skeleton for Repository Resource View flow
jobatzil/rename
Tan Nhu 2022-10-11 23:54:10 -07:00 committed by GitHub
parent 1cf07b6417
commit 6cadb048ba
31 changed files with 2896 additions and 2752 deletions

3
web/.gitignore vendored
View File

@ -2,4 +2,5 @@ node_modules
dist
coverage
.env
yarn-error*
yarn-error*
.DS_Store

View File

@ -35,7 +35,6 @@ module.exports = {
'./App': './src/App.tsx',
'./Welcome': './src/views/Welcome/Welcome.tsx',
'./Repos': './src/views/Repos/Repos.tsx',
'./NewRepo': './src/views/NewRepo/NewRepo.tsx',
'./RepoFiles': './src/views/RepoFiles/RepoFiles.tsx',
'./RepoFileDetails': './src/views/RepoFileDetails/RepoFileDetails.tsx',
'./RepoCommits': './src/views/RepoCommits/RepoCommits.tsx',

View File

@ -34,10 +34,10 @@ const devConfig = {
hot: true,
host: 'localhost',
historyApiFallback: true,
port: 3000,
port: 3001,
proxy: {
'/api': {
target: targetLocalHost ? 'http://localhost:3001' : baseUrl,
target: targetLocalHost ? 'http://localhost:3000' : baseUrl,
logLevel: 'debug',
secure: false,
changeOrigin: true

View File

@ -88,7 +88,6 @@
"react-popper": "^2.2.3",
"react-qr-code": "^1.1.1",
"react-router-dom": "^5.2.0",
"react-shadow": "^19.0.3",
"react-split-pane": "^0.1.92",
"react-table": "^7.1.0",
"react-table-sticky": "^1.1.3",

View File

@ -29,9 +29,10 @@ const App: React.FC<AppProps> = React.memo(function App({
}: AppProps) {
const [strings, setStrings] = useState<LanguageRecord>()
const [token, setToken] = useAPIToken(apiToken)
const getRequestOptions = useCallback((): Partial<RequestInit> => {
return buildResfulReactRequestOptions(hooks.useGetToken?.() || apiToken || 'default')
}, []) // eslint-disable-line react-hooks/exhaustive-deps
const getRequestOptions = useCallback(
(): Partial<RequestInit> => buildResfulReactRequestOptions(hooks.useGetToken?.() || apiToken || token),
[apiToken, token, hooks]
) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
languageLoader(lang).then(setStrings)

View File

@ -9,6 +9,8 @@ interface PathProps {
commitId?: string
}
interface QueryProps {}
export const pathProps: Readonly<Required<PathProps>> = {
accountId: ':accountId',
orgIdentifier: ':orgIdentifier',
@ -37,10 +39,6 @@ export default {
({ orgIdentifier, projectIdentifier }: PathProps) =>
`/scm/orgs/${orgIdentifier}/projects/${projectIdentifier}/repos`
),
toSCMNewRepo: withAccountId(
({ orgIdentifier, projectIdentifier }: PathProps) =>
`/scm/orgs/${orgIdentifier}/projects/${projectIdentifier}/repos/new`
),
toSCMRepoSettings: withAccountId(
({ orgIdentifier, projectIdentifier, repoName }: RequireField<PathProps, 'repoName'>) =>
`/scm/orgs/${orgIdentifier}/projects/${projectIdentifier}/repos/${repoName}/settings`

View File

@ -4,7 +4,6 @@ import { SignIn } from 'pages/SignIn/SignIn'
import { SignUp } from 'pages/SignUp/SignUp'
import routes, { pathProps } from 'RouteDefinitions'
import Welcome from 'views/Welcome/Welcome'
import NewRepo from 'views/NewRepo/NewRepo'
import Repos from 'views/Repos/Repos'
import RepoSettings from 'views/RepoSettings/RepoSettings'
import RepoFiles from 'views/RepoFiles/RepoFiles'
@ -27,9 +26,6 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
<Route path={routes.toSCMHome(pathProps)}>
<Welcome />
</Route>
<Route path={routes.toSCMNewRepo(pathProps)}>
<NewRepo />
</Route>
<Route path={routes.toSCMRepos(pathProps)} exact>
<Repos />
</Route>

View File

@ -9,6 +9,6 @@ import './bootstrap.scss'
window.STRIP_SCM_PREFIX = true
ReactDOM.render(
<App standalone accountId="default" apiToken="default" hooks={{}} components={{}} />,
<App standalone accountId="default" apiToken="" hooks={{}} components={{}} />,
document.getElementById('react-root')
)

View File

@ -3,17 +3,29 @@
* Use the command `yarn strings` to regenerate this file.
*/
export interface StringsMap {
addGitIgnore: string
addLicense: string
addReadMe: string
cancel: string
commits: string
content: string
create: string
createRepo: string
description: string
email: string
enterDescription: string
enterRepoName: string
existingAccount: string
failedToCreateRepo: string
files: string
history: string
name: string
newFile: string
newFolder: string
newRepo: string
noAccount: string
none: string
ok: string
pageNotFound: string
password: string
private: string
@ -21,11 +33,17 @@ export interface StringsMap {
pullRequests: string
'repos.activities': string
'repos.data': string
'repos.enterBranchName': string
'repos.lastChange': string
'repos.name': string
'repos.noDataMessage': string
'repos.noDataTitle': string
'repos.updated': string
repositories: string
search: string
settings: string
signIn: string
signUp: string
'validation.gitBranchNameInvalid': string
'validation.namePatternIsNotValid': string
}

View File

@ -1,6 +1,6 @@
import { useLocalStorage } from 'hooks/useLocalStorage'
const API_TOKEN_KEY = 'HARNESS_STANDALONE_APP_API_TOKEN'
const API_TOKEN_KEY = 'HARNESS_SCM_STANDALONE_APP_API_TOKEN'
/**
* Get and Set API token to use in Restful React calls.

View File

@ -9,6 +9,8 @@ public: Public
private: Private
cancel: Cancel
name: Name
search: Search
description: Description
repositories: Repositories
files: Files
commits: Commits
@ -18,9 +20,26 @@ newFile: New File
newFolder: New Folder
content: Content
history: History
newRepo: New Repository
createRepo: Create Repository
enterRepoName: Enter Repository Name
enterDescription: Enter a description (optional)
addLicense: Add License
none: None
create: Create
ok: OK
addGitIgnore: Add a .gitignore
addReadMe: Add a README file
failedToCreateRepo: Failed to create Repository. Please try again.
repos:
name: Repo Name
data: Repo Data
activities: Monthly Activities
updated: Updated Date
lastChange: Last Change
noDataTitle: No Repository Found
noDataMessage: Create a new repository by clicking the button below.
enterBranchName: Enter a branch name
validation:
namePatternIsNotValid: 'Name can only contain alphanumerics, . _ and -'
gitBranchNameInvalid: Branch name is invalid.

View File

@ -0,0 +1,17 @@
export interface Repository {
id: number
spaceId: number
pathName: string
path: string
name: string
description?: string
isPublic: boolean
createdBy: number
created: number
updated: number
forkId: number
numForks: number
numPulls: number
numClosedPulls: number
numOpenPulls: number
}

25
web/src/utils/GitUtils.ts Normal file
View File

@ -0,0 +1,25 @@
// Code copied from https://github.com/vweevers/is-git-ref-name-valid and
// https://github.com/vweevers/is-git-branch-name-valid (MIT, © Vincent Weevers)
// Last updated for git 2.29.0.
import type { IconName } from '@harness/icons'
// eslint-disable-next-line no-control-regex
const badGitRefRegrex = /(^|[/.])([/.]|$)|^@$|@{|[\x00-\x20\x7f~^:?*[\\]|\.lock(\/|$)/
const badGitBranchRegrex = /^(-|HEAD$)/
function isGitRefValid(name: string, onelevel: boolean): boolean {
return !badGitRefRegrex.test(name) && (!!onelevel || name.includes('/'))
}
export function isGitBranchNameValid(name: string): boolean {
return isGitRefValid(name, true) && !badGitBranchRegrex.test(name)
}
export const GitIcon: Readonly<Record<string, IconName>> = {
FILE: 'file',
REPOSITORY: 'git-repo',
COMMIT: 'git-branch-existing',
PULL_REQUEST: 'git-pull',
SETTINGS: 'cog'
}

View File

@ -5,7 +5,15 @@ import moment from 'moment'
import { useEffect } from 'react'
import { useAppContext } from 'AppContext'
export const LIST_FETCHING_PAGE_SIZE = 20
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
export const X_TOTAL = 'x-total'
export const X_TOTAL_PAGES = 'x-total-pages'
export const X_PER_PAGE = 'x-per-page'
export type Unknown = any // eslint-disable-line @typescript-eslint/no-explicit-any
export const DEFAULT_BRANCH_NAME = 'main'
export const REGEX_VALID_REPO_NAME = /^[A-Za-z0-9_.-][A-Za-z0-9 _.-]*$/
export const SUGGESTED_BRANCH_NAMES = ['main', 'master']
/** This utility shows a toaster without being bound to any component.
* It's useful to show cross-page/component messages */
@ -25,7 +33,7 @@ export const MonacoEditorOptions = {
codeLens: false,
scrollBeyondLastLine: false,
smartSelect: false,
tabSize: 4,
tabSize: 2,
insertSpaces: true,
overviewRulerBorder: false
}
@ -45,13 +53,38 @@ export const deselectAllMonacoEditor = (editor?: EDITOR.IStandaloneCodeEditor):
}, 0)
}
export const LIST_FETCHING_PAGE_SIZE = 20
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
export const displayDateTime = (value: number): string | null => {
return value ? moment.unix(value / 1000).format(DEFAULT_DATE_FORMAT) : null
}
const LOCALE = Intl.NumberFormat().resolvedOptions?.().locale || 'en-US'
/**
* Format a timestamp to short format time (i.e: 7:41 AM)
* @param timestamp Timestamp
* @param timeStyle Optional DateTimeFormat's `timeStyle` option.
*/
export function formatTime(timestamp: number, timeStyle = 'short'): string {
return new Intl.DateTimeFormat(LOCALE, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: TS built-in type for DateTimeFormat is not correct
timeStyle
}).format(new Date(timestamp))
}
/**
* Format a timestamp to medium format date (i.e: Jan 1, 2021)
* @param timestamp Timestamp
* @param dateStyle Optional DateTimeFormat's `dateStyle` option.
*/
export function formatDate(timestamp: number, dateStyle = 'medium'): string {
return new Intl.DateTimeFormat(LOCALE, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: TS built-in type for DateTimeFormat is not correct
dateStyle
}).format(new Date(timestamp))
}
export enum Editions {
ENTERPRISE = 'ENTERPRISE',
TEAM = 'TEAM',

View File

@ -1,3 +0,0 @@
.main {
padding: var(--spacing-xlarge);
}

View File

@ -1,7 +0,0 @@
import React from 'react'
import { Container } from '@harness/uicore'
import css from './NewRepo.module.scss'
export default function NewRepo(): JSX.Element {
return <Container className={css.main}>NewRepo!</Container>
}

View File

@ -5,6 +5,7 @@ import SplitPane from 'react-split-pane'
import { PopoverInteractionKind } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import { ButtonRoleProps } from 'utils/Utils'
import { GitIcon } from 'utils/GitUtils'
import { ResourceTree } from './ResourceTree/ResourceTree'
import { ResourceContent } from './ResourceContent/ResourceContent'
import css from './RepoFiles.module.scss'
@ -27,7 +28,7 @@ export default function RepoFiles(): JSX.Element {
<Text
inline
className={css.repoDropdown}
icon="git-repo"
icon={GitIcon.REPOSITORY}
iconProps={{
size: 14,
color: Color.GREY_500,
@ -60,28 +61,28 @@ export default function RepoFiles(): JSX.Element {
id: 'files',
title: getString('files'),
panel: <Files />,
iconProps: { name: 'file' },
iconProps: { name: GitIcon.FILE },
disabled: false
},
{
id: 'commits',
title: getString('commits'),
panel: <Commits />,
iconProps: { name: 'git-branch-existing' },
iconProps: { name: GitIcon.COMMIT },
disabled: true
},
{
id: 'pull-requests',
title: getString('pullRequests'),
panel: <PullRequests />,
iconProps: { name: 'git-pull' },
iconProps: { name: GitIcon.PULL_REQUEST },
disabled: true
},
{
id: 'settings',
title: getString('settings'),
panel: <Settings />,
iconProps: { name: 'cog' },
iconProps: { name: GitIcon.SETTINGS },
disabled: true
}
]}></Tabs>

View File

@ -1,7 +1,7 @@
import React, { useMemo, useRef, useState } from 'react'
import cx from 'classnames'
// import root from 'react-shadow'
// import MonacoEditor from 'react-monaco-editor'
import MonacoEditor from 'react-monaco-editor'
import type { editor as EDITOR } from 'monaco-editor/esm/vs/editor/editor.api'
import {
Container,
@ -22,8 +22,9 @@ import { useStrings } from 'framework/strings'
// import markdownCSS from '!raw-loader!@uiw/react-markdown-preview/dist/markdown.css'
import markdown from './sampleREADME.md'
import css from './ResourceContent.module.scss'
import sampleCSs from '!raw-loader!./ResourceContent.module.scss'
import('!raw-loader!./sampleREADME.md').then(foo => console.log('dynamic data:', foo.default))
// import('!raw-loader!./sampleREADME.md').then(foo => console.log('dynamic data:', foo.default))
// TODO: USE FROM SERVICE (DOES NOT EXIST YET)
interface Folder {
@ -169,9 +170,11 @@ export function FolderListing(): JSX.Element {
const inputContainerRef = useRef<HTMLDivElement>(null)
const [inputEditor, setInputEditor] = useState<EDITOR.IStandaloneCodeEditor>()
console.log({ sampleCSs })
return (
<Container>
<Table<Folder>
{/* <Table<Folder>
className={css.table}
columns={columns}
data={repodata || []}
@ -179,7 +182,7 @@ export function FolderListing(): JSX.Element {
// onPolicyClicked(data)
}}
getRowClassName={() => css.row}
/>
/> */}
<Container className={css.fileContentContainer} background={Color.WHITE}>
<Layout.Horizontal padding="small" className={css.fileContentHeading}>
@ -187,27 +190,20 @@ export function FolderListing(): JSX.Element {
<FlexExpander />
<Button variation={ButtonVariation.ICON} icon="edit" />
</Layout.Horizontal>
<Container className={css.readmeContainer}>
{/* <root.div style={{ all: 'initial' }}> */}
<Container className={css.readmeContainer} style={{ display: 'none' }}>
<MarkdownPreview
source={markdown}
rehypeRewrite={(node, _index, parent) => {
if (
(node as unknown as Element).tagName === 'a' &&
parent &&
/^h(1|2|3|4|5|6)/.test((parent as unknown as Element).tagName)
) {
parent.children = parent.children.slice(1)
}
}}
// rehypeRewrite={(node, _index, parent) => {
// if (
// (node as unknown as Element).tagName === 'a' &&
// parent &&
// /^h(1|2|3|4|5|6)/.test((parent as unknown as Element).tagName)
// ) {
// parent.children = parent.children.slice(1)
// }
// }}
/>
{/* <style type="text/css">{markdownCSS}</style>
</root.div> */}
{/* <Container
flex
// className={css.ioEditor}
ref={inputContainerRef}
style={{ maxHeight: '95%', height: '900px' }}>
<Container flex ref={inputContainerRef} style={{ maxHeight: '95%', height: '900px' }}>
<MonacoEditor
language="json"
theme="vs-light"
@ -216,7 +212,7 @@ export function FolderListing(): JSX.Element {
onChange={updateInput}
editorDidMount={setInputEditor}
/>
</Container> */}
</Container>
</Container>
</Container>
</Container>

View File

@ -0,0 +1,3 @@
.divider {
margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important;
}

View File

@ -1,6 +1,6 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly divider: string
}
export default styles

View File

@ -5,8 +5,8 @@
* https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
*/
import React, { useState } from 'react'
import { Dialog, Intent } from '@blueprintjs/core'
import React, { useMemo, useState } from 'react'
import { Icon as BPIcon, Classes, Dialog, Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core'
import * as yup from 'yup'
import {
Button,
@ -20,29 +20,39 @@ import {
Heading,
useToaster,
FormInput,
Text
Text,
ButtonVariation,
ButtonSize,
TextInput
} from '@harness/uicore'
import { FontVariation } from '@harness/design-system'
import { useMutate } from 'restful-react'
import { get } from 'lodash-es'
import { useModalHook } from '@harness/use-modal'
import { useStrings } from 'framework/strings'
import { getErrorMessage, Unknown } from 'utils/Utils'
import {
DEFAULT_BRANCH_NAME,
getErrorMessage,
REGEX_VALID_REPO_NAME,
SUGGESTED_BRANCH_NAMES,
Unknown
} from 'utils/Utils'
import type { Repository } from 'types/Repository'
import { isGitBranchNameValid } from 'utils/GitUtils'
import licences from './licences'
import gitignores from './gitignores'
import css from './NewRepoModalButton.module.scss'
export interface NewRepoModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'> {
accountIdentifier: string
orgIdentifier: string
projectIdentifier: string
space: string
modalTitle: string
submitButtonTitle?: string
cancelButtonTitle?: string
onSubmit: (data: Unknown) => void
onSubmit: (data: Repository) => void
}
export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
accountIdentifier,
orgIdentifier,
projectIdentifier,
space,
modalTitle,
submitButtonTitle,
cancelButtonTitle,
@ -51,23 +61,47 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
}) => {
const ModalComponent: React.FC = () => {
const { getString } = useStrings()
const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME)
const { showError } = useToaster()
const { mutate: createRepo, loading } = useMutate({
verb: 'POST',
path: '/api/v1/repos'
})
const [submitLoading, setSubmitLoading] = useState(false)
const handleSubmit = (_data?: Unknown): void => {
setSubmitLoading(true)
const handleSubmit = (formData?: Unknown): void => {
try {
onSubmit({})
// TODO: Backend is lacking support for these fields except
// name and description (Oct 5)
createRepo({
pathName: get(formData, 'name', '').trim(),
name: get(formData, 'name', '').trim(),
spaceId: space,
description: get(formData, 'description', '').trim(),
isPublic: false,
license: get(formData, 'license', 'none'),
gitIgnore: get(formData, 'gitignore', 'none'),
defaultBranch: get(formData, 'defaultBranch', 'main'),
addReadme: get(formData, 'addReadme', false)
})
.then(response => {
hideModal()
onSubmit(response)
})
.catch(_error => {
showError(getErrorMessage(_error), 0, 'failedToCreateRepo')
})
} catch (exception) {
setSubmitLoading(false)
showError(getErrorMessage(exception), 0, 'cf.save.ff.error')
showError(getErrorMessage(exception), 0, 'failedToCreateRepo')
}
}
const loading = submitLoading
return (
<Dialog isOpen enforceFocus={false} onClose={hideModal} title={''} style={{ width: 700, maxHeight: '95vh' }}>
<Dialog
isOpen
enforceFocus={false}
onClose={hideModal}
title={''}
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
<Layout.Vertical
padding={{ left: 'xxlarge' }}
style={{ height: '100%' }}
@ -79,13 +113,21 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
<Container margin={{ right: 'xxlarge' }}>
<Formik
initialValues={{
gitDetails: '',
autoCommit: ''
name: '',
description: '',
license: '',
defaultBranch: 'main',
gitignore: '',
addReadme: false
}}
formName="editVariations"
enableReinitialize={true}
validationSchema={yup.object().shape({
// gitDetails: 'gitSyncFormMeta?.gitSyncValidationSchema'
name: yup
.string()
.trim()
.required()
.matches(REGEX_VALID_REPO_NAME, getString('validation.namePatternIsNotValid'))
})}
validateOnChange
validateOnBlur
@ -93,8 +135,8 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
<FormikForm>
<FormInput.Text
name="name"
label="Name"
placeholder="Enter Repository Name"
label={getString('name')}
placeholder={getString('enterRepoName')}
tooltipProps={{
dataTooltipId: 'repositoryNameTextField'
}}
@ -102,51 +144,50 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
/>
<FormInput.Text
name="description"
label="Description"
placeholder="Enter a description (optional)"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'repositoryDescriptionTextField'
}}
/>
<Container margin={{ top: 'medium', bottom: 'xlarge' }}>
<Text>
Your repository will be initialized with a <strong>main</strong> branch.
Your repository will be initialized with a{' '}
<strong>
<Button
text={branchName}
icon="git-new-branch"
rightIcon="chevron-down"
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
iconProps={{ size: 14 }}
tooltip={<BranchName currentBranchName={branchName} onSelect={name => setBranchName(name)} />}
tooltipProps={{ interactionKind: 'click' }}
/>
</strong>{' '}
branch.
</Text>
</Container>
<FormInput.Select
name="license"
label="Add License"
placeholder="None"
items={[
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue' },
{
label: 'TryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTrying',
value: 'xyz'
},
{ label: 'Trying a long phrase with spaces to try out different combinations', value: 'abcd' }
]}
label={getString('addLicense')}
placeholder={getString('none')}
items={licences}
usePortal
/>
<FormInput.Select
name="gitignore"
label="Add a .gitignore"
placeholder="None"
items={[
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue' },
{
label: 'TryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTryingTrying',
value: 'xyz'
},
{ label: 'Trying a long phrase with spaces to try out different combinations', value: 'abcd' }
]}
label={getString('addGitIgnore')}
placeholder={getString('none')}
items={gitignores}
usePortal
/>
<FormInput.CheckBox
name="addReadme"
label="Add a README file"
label={getString('addReadMe')}
tooltipProps={{
dataTooltipId: 'addReadMe'
}}
@ -155,7 +196,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
spacing="small"
padding={{ right: 'xxlarge', top: 'xxxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={'Create Repository'} intent={Intent.PRIMARY} disabled={loading} />
<Button type="submit" text={getString('createRepo')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
@ -173,3 +214,55 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
return <Button onClick={openModal} {...props} />
}
interface BranchNameProps {
currentBranchName: string
onSelect: (branchName: string) => void
}
const BranchName: React.FC<BranchNameProps> = ({ currentBranchName, onSelect }) => {
const { getString } = useStrings()
const [customName, setCustomName] = useState(
SUGGESTED_BRANCH_NAMES.includes(currentBranchName) ? '' : currentBranchName
)
const isCustomNameValid = useMemo(() => !customName || isGitBranchNameValid(customName), [customName])
return (
<Container padding="medium" width={250}>
<Layout.Vertical spacing="small">
<Menu>
{SUGGESTED_BRANCH_NAMES.map(name => (
<MenuItem
key={name}
text={name}
labelElement={name === currentBranchName ? <BPIcon icon="small-tick" /> : undefined}
disabled={name === currentBranchName}
onClick={() => onSelect(name)}
/>
))}
<MenuDivider className={css.divider} />
<TextInput
defaultValue={customName}
autoFocus
placeholder={getString('repos.enterBranchName')}
onInput={e => setCustomName((e.currentTarget.value || '').trim())}
errorText={isCustomNameValid ? undefined : getString('validation.gitBranchNameInvalid')}
intent={!customName ? undefined : isCustomNameValid ? 'success' : 'danger'}
/>
</Menu>
<Container>
<Layout.Horizontal>
<Button
type="submit"
text={getString('ok')}
intent={Intent.PRIMARY}
className={Classes.POPOVER_DISMISS}
disabled={!customName || !isCustomNameValid}
onClick={() => onSelect(customName)}
/>
<Button text={getString('cancel')} minimal className={Classes.POPOVER_DISMISS} />
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Container>
)
}

View File

@ -3,6 +3,10 @@
background-color: var(--primary-bg) !important;
}
.withError {
display: grid;
}
.table {
[class*='TableV2--header'] [class*='variation-table-headers'] {
text-transform: none;

View File

@ -2,6 +2,7 @@
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly withError: string
readonly table: string
readonly row: string
readonly nameContainer: string

View File

@ -13,52 +13,17 @@ import {
Pagination
} from '@harness/uicore'
import type { CellProps, Column } from 'react-table'
import cx from 'classnames'
import { useGet } from 'restful-react'
import { useStrings } from 'framework/strings'
import { formatDate, getErrorMessage, X_PER_PAGE, X_TOTAL, X_TOTAL_PAGES } from 'utils/Utils'
import type { Repository } from 'types/Repository'
import { NewRepoModalButton } from './NewRepoModalButton'
import { PinnedRibbon } from './PinnedRibbon'
import chartImg from './chart.svg'
import emptyStateImage from './empty_state.png'
import css from './Repos.module.scss'
// TODO: USE FROM SERVICE (DOES NOT EXIST YET)
interface Repository {
accountId: string
name: string
description?: string
pinned?: boolean
public?: boolean
metadata?: {
language: string
branch: number
commit: number
fork: number
}
activities: string
updated: number
}
const repodata: Repository[] = [...Array(50).keys()].map(number => ({
accountId: `Harness`,
name: `Repo ${number}`,
description:
number < 4
? 'Repo very long name very long name long long long longRepo very long name very long name long long long long'
: '',
data: `Data ${number}`,
pinned: number < 3,
public: number < 2,
metadata:
number % 2
? {
language: 'Java',
branch: 1000,
commit: 12323,
fork: 2
}
: undefined,
activities: `Activity ${number}`,
updated: Date.now() - 40000
}))
function RepoMetadata(): JSX.Element {
return (
<Container width="70%">
@ -79,6 +44,20 @@ export default function Repos(): JSX.Element {
const { getString } = useStrings()
const ref = useRef<HTMLDivElement>(null)
const [nameTextWidth, setNameTextWidth] = useState(300)
const space = 3
const {
data: repositories,
error,
loading,
refetch,
response
} = useGet<Repository[]>({
path: `/api/v1/spaces/${space}/repos`
})
const itemCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL) || '0'), [response])
const pageCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL_PAGES) || '0'), [response])
const pageSize = useMemo(() => parseInt(response?.headers?.get(X_PER_PAGE) || '0'), [response])
const columns: Column<Repository>[] = useMemo(
() => [
{
@ -140,7 +119,7 @@ export default function Repos(): JSX.Element {
Cell: ({ row }: CellProps<Repository>) => {
return (
<Text color={Color.BLACK} lineClamp={1}>
{'July 13, 2022' || row.original.updated}
{formatDate(row.original.updated)}
</Text>
)
},
@ -155,66 +134,79 @@ export default function Repos(): JSX.Element {
setNameTextWidth((ref.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
}
}
const NewRepoButton = (
<NewRepoModalButton
space={space}
modalTitle={getString('newRepo')}
text={getString('newRepo')}
variation={ButtonVariation.PRIMARY}
icon="plus"
onSubmit={_data => {
// TODO: navigate to Repo instead of refetch
refetch()
}}
/>
)
useEffect(() => {
onResize()
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
}, [])
const itemCount = repodata.length
const pageSize = 25
const pageCount = itemCount
console.log({ response, repositories, pageCount, pageIndex, pageSize, itemCount })
return (
<Container className={css.main}>
<PageHeader title={getString('repositories')} />
<PageBody>
<PageBody
loading={loading}
className={cx({ [css.withError]: !!error })}
error={error ? getErrorMessage(error) : null}
retryOnError={() => {
refetch()
}}
noData={{
when: () => repositories?.length === 0,
image: emptyStateImage,
messageTitle: getString('repos.noDataTitle'),
message: getString('repos.noDataMessage'),
button: NewRepoButton
}}>
<Container padding="xlarge">
<Layout.Horizontal spacing="large">
{/* <Button text="New Repository" variation={ButtonVariation.PRIMARY} icon="book" /> */}
<NewRepoModalButton
accountIdentifier=""
orgIdentifier=""
projectIdentifier=""
modalTitle="Create Repository"
text="New Repository"
variation={ButtonVariation.PRIMARY}
icon="plus"
onSubmit={_data => {
console.log(_data)
}}
/>
{NewRepoButton}
<FlexExpander />
<TextInput placeholder="Search" leftIcon="search" style={{ width: 350 }} autoFocus />
<TextInput placeholder={getString('search')} leftIcon="search" style={{ width: 350 }} autoFocus />
</Layout.Horizontal>
<Container margin={{ top: 'medium' }}>
<Table<Repository>
rowDataTestID={(_, index: number) => `scm-repo-${index}`}
className={css.table}
columns={columns}
data={repodata || []}
data={repositories || []}
onRowClick={_data => {
// onPolicyClicked(data)
}}
getRowClassName={() => css.row}
/>
</Container>
<Container margin={{ bottom: 'medium', left: 'xxxlarge', right: 'xxxlarge' }}>
<Pagination
className={css.pagination}
hidePageNumbers
gotoPage={index => setPageIndex(index)}
itemCount={itemCount}
pageCount={pageCount}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[5, 10, 20, 40]}
/>
</Container>
{!!repositories?.length && (
<Container margin={{ bottom: 'medium', left: 'xxxlarge', right: 'xxxlarge' }}>
<Pagination
className={css.pagination}
hidePageNumbers
gotoPage={index => setPageIndex(index)}
itemCount={itemCount}
pageCount={pageCount}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[5, 10, 20, 40]}
/>
</Container>
)}
</Container>
</PageBody>
</Container>

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,130 @@
export default [
'None',
'Actionscript',
'Ada',
'Agda',
'Android',
'AppEngine',
'AppceleratorTitanium',
'ArchLinuxPackages',
'Autotools',
'C',
'C++',
'CFWheels',
'CMake',
'CUDA',
'CakePHP',
'ChefCookbook',
'Clojure',
'CodeIgniter',
'CommonLisp',
'Composer',
'Concrete5',
'Coq',
'CraftCMS',
'D',
'DM',
'Dart',
'Delphi',
'Drupal',
'EPiServer',
'Eagle',
'Elisp',
'Elixir',
'Elm',
'Erlang',
'ExpressionEngine',
'ExtJs',
'Fancy',
'Finale',
'ForceDotCom',
'Fortran',
'FuelPHP',
'GWT',
'GitBook',
'Go',
'Godot',
'Gradle',
'Grails',
'Haskell',
'IGORPro',
'Idris',
'JENKINS_HOME',
'Java',
'Jboss',
'Jekyll',
'Joomla',
'Julia',
'KiCAD',
'Kohana',
'Kotlin',
'LabVIEW',
'Laravel',
'Leiningen',
'LemonStand',
'Lilypond',
'Lithium',
'Lua',
'Magento',
'Maven',
'Mercury',
'MetaProgrammingSystem',
'Nim',
'Node',
'OCaml',
'Objective-C',
'Opa',
'OracleForms',
'Packer',
'Perl',
'Perl6',
'Phalcon',
'PlayFramework',
'Plone',
'Prestashop',
'Processing',
'PureScript',
'Python',
'Qooxdoo',
'Qt',
'R',
'ROS',
'Rails',
'RhodesRhomobile',
'Ruby',
'Rust',
'SCons',
'Sass',
'Scala',
'Scheme',
'Scrivener',
'Sdcc',
'SeamGen',
'SketchUp',
'Smalltalk',
'SugarCRM',
'Swift',
'Symfony',
'SymphonyCMS',
'TeX',
'Terraform',
'Textpattern',
'TurboGears2',
'Typo3',
'Umbraco',
'Unity',
'UnrealEngine',
'VVVV',
'VisualStudio',
'Waf',
'WordPress',
'Xojo',
'Yeoman',
'Yii',
'ZendFramework',
'Zephir',
'gcov',
'nanoc',
'opencart',
'stella'
].map(entry => ({ label: entry, value: entry }))

View File

@ -0,0 +1,31 @@
export default [
{ label: 'None', value: 'none' },
{ label: 'Academic Free License v3.0', value: 'afl-3.0' },
{ label: 'Apache license 2.0', value: 'apache-2.0' },
{ label: 'Artistic license 2.0', value: 'artistic-2.0' },
{ label: 'Boost Software License 1.0', value: 'bsl-1.0' },
{ label: 'BSD 2-clause "Simplified" license', value: 'bsd-2-clause' },
{ label: 'BSD 3-clause "New" or "Revised" license', value: 'bsd-3-clause' },
{ label: 'BSD 3-clause Clear license', value: 'bsd-3-clause-clear' },
{ label: 'Creative Commons license family', value: 'cc' },
{ label: 'Creative Commons Zero v1.0 Universal', value: 'cc0-1.0' },
{ label: 'Creative Commons Attribution 4.0', value: 'cc-by-4.0' },
{ label: 'Creative Commons Attribution Share Alike 4.0', value: 'cc-by-sa-4.0' },
{ label: 'Educational Community License v2.0', value: 'ecl-2.0' },
{ label: 'Eclipse Public License 1.0', value: 'epl-1.0' },
{ label: 'Eclipse Public License 2.0', value: 'epl-2.0' },
{ label: 'European Union Public License 1.1', value: 'eupl-1.1' },
{ label: 'GNU Affero General Public License v3.0', value: 'agpl-3.0' },
{ label: 'GNU General Public License family', value: 'gpl' },
{ label: 'GNU General Public License v2.0', value: 'gpl-2.0' },
{ label: 'GNU General Public License v3.0', value: 'gpl-3.0' },
{ label: 'GNU Lesser General Public License family', value: 'lgpl' },
{ label: 'GNU Lesser General Public License v2.1', value: 'lgpl-2.1' },
{ label: 'GNU Lesser General Public License v3.0', value: 'lgpl-3.0' },
{ label: 'ISC', value: 'isc' },
{ label: 'MIT', value: 'mit' },
{ label: 'Mozilla Public License 2.0', value: 'mpl-2.0' },
{ label: 'Open Software License 3.0', value: 'osl-3.0' },
{ label: 'The Unlicense', value: 'unlicense' },
{ label: 'zLib License', value: 'zlib' }
]

View File

@ -1,3 +0,0 @@
.main {
padding: var(--spacing-xlarge);
}

View File

@ -1,12 +0,0 @@
/* eslint-disable */
/**
* Copyright 2021 Harness Inc. All rights reserved.
* Use of this source code is governed by the PolyForm Shield 1.0.0 license
* that can be found in the licenses directory at the root of this repository, also available at
* https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
**/
// this is an auto-generated file, do not update this manually
declare const styles: {
readonly main: string
}
export default styles

View File

@ -1,7 +0,0 @@
import React from 'react'
import { Container } from '@harness/uicore'
import css from './TestView.module.scss'
export const TestView: React.FC = () => {
return <Container className={css.main}>TestView</Container>
}

File diff suppressed because it is too large Load Diff