Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
小 白蛋
Clutch
Commits
883c1df0
Unverified
Commit
883c1df0
authored
4 years ago
by
Scarlett Perry
Committed by
GitHub
4 years ago
Browse files
Options
Download
Email Patches
Plain Diff
project selector: project api integration (#1538)
parent
32839a91
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
frontend/packages/core/src/index.tsx
+2
-0
frontend/packages/core/src/index.tsx
frontend/workflows/projectSelector/src/hello-world.tsx
+53
-36
frontend/workflows/projectSelector/src/hello-world.tsx
frontend/workflows/projectSelector/src/selector-reducer.tsx
+81
-18
frontend/workflows/projectSelector/src/selector-reducer.tsx
with
136 additions
and
54 deletions
+136
-54
frontend/packages/core/src/index.tsx
+
2
-
0
View file @
883c1df0
import
{
Grid
}
from
"
@material-ui/core
"
;
import
{
userId
}
from
"
./AppLayout/user
"
;
import
{
Checkbox
,
CheckboxPanel
}
from
"
./Input/checkbox
"
;
import
{
Form
,
FormRow
}
from
"
./Input/form
"
;
import
Radio
from
"
./Input/radio
"
;
...
...
@@ -109,6 +110,7 @@ export {
TextField
,
TreeTable
,
Typography
,
userId
,
useWizardContext
,
Warning
,
WizardContext
,
...
...
This diff is collapsed.
Click to expand it.
frontend/workflows/projectSelector/src/hello-world.tsx
+
53
-
36
View file @
883c1df0
import
*
as
React
from
"
react
"
;
import
{
TextField
}
from
"
@clutch-sh/core
"
;
import
type
{
clutch
as
IClutch
}
from
"
@clutch-sh/api
"
;
import
type
{
ClutchError
}
from
"
@clutch-sh/core
"
;
import
{
client
,
TextField
,
userId
}
from
"
@clutch-sh/core
"
;
import
styled
from
"
@emotion/styled
"
;
import
{
Divider
,
LinearProgress
}
from
"
@material-ui/core
"
;
import
LayersIcon
from
"
@material-ui/icons/Layers
"
;
...
...
@@ -31,7 +33,7 @@ interface UserPayload {
projects
?:
string
[];
}
type
BackgroundActionKind
=
"
HYDRATE_START
"
|
"
HYDRATE_END
"
;
type
BackgroundActionKind
=
"
HYDRATE_START
"
|
"
HYDRATE_END
"
|
"
HYDRATE_ERROR
"
;
interface
BackgroundAction
{
type
:
BackgroundActionKind
;
...
...
@@ -49,14 +51,9 @@ export interface State {
[
Group
.
UPSTREAM
]:
GroupState
;
[
Group
.
DOWNSTREAM
]:
GroupState
;
projectData
:
{
[
projectName
:
string
]:
Project
};
projectData
:
{
[
projectName
:
string
]:
IClutch
.
core
.
project
.
v1
.
I
Project
};
loading
:
boolean
;
}
// TODO: subout with full manifest structure (from proto def)
interface
Project
{
upstreams
:
string
[];
downstreams
:
string
[];
error
:
ClutchError
|
undefined
;
}
interface
GroupState
{
...
...
@@ -80,15 +77,6 @@ export const useDispatch = () => {
return
React
.
useContext
(
DispatchContext
);
};
const
fakeAPI
=
(
state
:
State
)
=>
{
return
{
clutch
:
{
upstreams
:
[],
downstreams
:
[
"
rides
"
,
"
locations
"
],
},
};
};
// TODO(perf): call with useMemo().
export
const
deriveSwitchStatus
=
(
state
:
State
,
group
:
Group
):
boolean
=>
{
return
(
...
...
@@ -103,6 +91,7 @@ const initialState: State = {
[
Group
.
DOWNSTREAM
]:
{},
projectData
:
{},
loading
:
false
,
error
:
undefined
,
};
const
StyledSelectorContainer
=
styled
.
div
({
...
...
@@ -112,11 +101,10 @@ const StyledSelectorContainer = styled.div({
width
:
"
245px
"
,
});
// TODO: change icon, center align icon and title
const
StyledWorkflowHeader
=
styled
.
div
({
margin
:
"
16px 16px 12px 16px
"
,
display
:
"
flex
"
,
alignItems
:
"
center
"
alignItems
:
"
center
"
,
});
const
StyledWorkflowTitle
=
styled
.
span
({
...
...
@@ -126,7 +114,6 @@ const StyledWorkflowTitle = styled.span({
margin
:
"
0px 8px
"
,
});
// TODO: add plus icon in the text field
const
StyledProjectTextField
=
styled
(
TextField
)({
padding
:
"
16px 16px 8px 16px
"
,
});
...
...
@@ -138,15 +125,15 @@ const StyledProgressContainer = styled.div({
},
"
.MuiLinearProgress-bar
"
:
{
backgroundColor
:
"
#3548D4
"
,
}
}
,
});
const
ProjectSelector
=
()
=>
{
// On load, we'll request a list of owned projects and their upstreams and downstreams from the API.
// The API will contain information about the relationships between projects and upstreams and downstreams.
// By default, the owned projects will be checked and others will be unchecked.
// If a project is unchecked, the upstream and downstreams related to it disappear from the list.
// If a project is rechecked, the checks were preserved.
//
TODO:
If a project is unchecked, the upstream and downstreams related to it disappear from the list.
//
TODO:
If a project is rechecked, the checks were preserved.
const
[
customProject
,
setCustomProject
]
=
React
.
useState
(
""
);
...
...
@@ -161,6 +148,12 @@ const ProjectSelector = () => {
let
allPresent
=
true
;
_
.
forEach
(
Object
.
keys
(
state
[
Group
.
PROJECTS
]),
p
=>
{
/*
TODO: b/c of this conditional, if a user adds an upstream/downstream we already have the project data for
to the custom project group, allPresent will be true and we wont trigger an api call. One way to account for this
is updating the conditional to additionally check if the project is included in state[Group.Downstreams]/state[Group.Upstreams]
and if so, mark allPresent as false.
*/
if
(
!
(
p
in
state
.
projectData
))
{
allPresent
=
false
;
return
false
;
// Stop iteration.
...
...
@@ -171,11 +164,29 @@ const ProjectSelector = () => {
if
(
!
state
.
loading
&&
(
Object
.
keys
(
state
[
Group
.
PROJECTS
]).
length
==
0
||
!
allPresent
))
{
console
.
log
(
"
calling API!
"
,
state
.
loading
);
dispatch
({
type
:
"
HYDRATE_START
"
});
// TODO: call API and use payload.
setTimeout
(
()
=>
dispatch
({
type
:
"
HYDRATE_END
"
,
payload
:
{
result
:
fakeAPI
(
state
)
}
}),
1000
);
// TODO: have userId check be server driven
const
requestParams
=
{
users
:
[
userId
()],
projects
:
[]
};
_
.
forEach
(
Object
.
keys
(
state
[
Group
.
PROJECTS
]),
p
=>
{
// if the project is custom and missing from state.projectdata
if
(
state
[
Group
.
PROJECTS
][
p
].
custom
&&
!
(
p
in
state
.
projectData
))
{
requestParams
.
projects
.
push
(
p
);
}
});
/*
TODO: the API doesn't return an error if a custom project is not found so we should first
check if the API returns empty results and process that as an error
*/
client
.
post
(
"
/v1/project/getProjects
"
,
requestParams
as
IClutch
.
project
.
v1
.
GetProjectsRequest
)
.
then
(
resp
=>
{
const
{
results
}
=
resp
.
data
as
IClutch
.
project
.
v1
.
GetProjectsResponse
;
dispatch
({
type
:
"
HYDRATE_END
"
,
payload
:
{
result
:
results
||
{}
}
});
})
.
catch
((
err
:
ClutchError
)
=>
{
dispatch
({
type
:
"
HYDRATE_ERROR
"
,
payload
:
{
result
:
err
}
});
});
}
},
[
state
[
Group
.
PROJECTS
]]);
...
...
@@ -190,11 +201,14 @@ const ProjectSelector = () => {
setCustomProject
(
""
);
};
const
hasError
=
state
.
error
!==
undefined
&&
state
.
error
!==
null
;
return
(
<
DispatchContext
.
Provider
value
=
{
dispatch
}
>
<
StateContext
.
Provider
value
=
{
state
}
>
<
StyledSelectorContainer
>
<
StyledWorkflowHeader
>
{
/* TODO: change icon to match design */
}
<
LayersIcon
/>
<
StyledWorkflowTitle
>
Dash
</
StyledWorkflowTitle
>
</
StyledWorkflowHeader
>
...
...
@@ -202,13 +216,16 @@ const ProjectSelector = () => {
{
state
.
loading
&&
<
LinearProgress
color
=
"secondary"
/>
}
</
StyledProgressContainer
>
<
Divider
/>
<
StyledProjectTextField
disabled
=
{
state
.
loading
}
placeholder
=
"Add a project"
value
=
{
customProject
}
onChange
=
{
e
=>
setCustomProject
(
e
.
target
.
value
)
}
onKeyDown
=
{
e
=>
e
.
key
===
"
Enter
"
&&
handleAdd
()
}
/>
{
/* TODO: add plus icon in the text field */
}
<
StyledProjectTextField
disabled
=
{
state
.
loading
}
placeholder
=
"Add a project"
value
=
{
customProject
}
onChange
=
{
e
=>
setCustomProject
(
e
.
target
.
value
)
}
onKeyDown
=
{
e
=>
e
.
key
===
"
Enter
"
&&
handleAdd
()
}
helperText
=
{
state
.
error
?.
message
}
error
=
{
hasError
}
/>
<
ProjectGroup
title
=
"Projects"
group
=
{
Group
.
PROJECTS
}
displayToggleHelperText
/>
<
Divider
/>
<
ProjectGroup
title
=
"Upstreams"
group
=
{
Group
.
UPSTREAM
}
/>
...
...
This diff is collapsed.
Click to expand it.
frontend/workflows/projectSelector/src/selector-reducer.tsx
+
81
-
18
View file @
883c1df0
import
type
{
clutch
as
IClutch
}
from
"
@clutch-sh/api
"
;
import
_
from
"
lodash
"
;
import
type
{
Action
,
State
}
from
"
./hello-world
"
;
...
...
@@ -5,6 +6,8 @@ import { deriveSwitchStatus, Group } from "./hello-world";
const
selectorReducer
=
(
state
:
State
,
action
:
Action
):
State
=>
{
switch
(
action
.
type
)
{
// User actions.
case
"
ADD_PROJECTS
"
:
{
// a given custom project may already exist in the group so don't trigger a state update for those duplicates
const
uniqueCustomProjects
=
action
.
payload
.
projects
.
filter
(
...
...
@@ -75,33 +78,93 @@ const selectorReducer = (state: State, action: Action): State => {
return
newGroupToggledState
;
}
// Background actions.
case
"
HYDRATE_START
"
:
{
return
{
...
state
,
loading
:
true
};
}
case
"
HYDRATE_END
"
:
{
const
newPostAPICallState
=
{
...
state
,
loading
:
false
};
// TODO: handle payload.
_
.
forIn
(
action
.
payload
.
result
,
(
v
,
k
)
=>
{
// Add each project to the projects list.
state
[
Group
.
PROJECTS
][
k
]
=
{
checked
:
true
};
state
.
projectData
[
k
]
=
{};
const
newPostAPICallState
=
{
...
state
,
loading
:
false
,
error
:
undefined
};
_
.
forIn
(
action
.
payload
.
result
as
IClutch
.
project
.
v1
.
IGetProjectsResponse
,
(
v
:
IClutch
.
project
.
v1
.
IProjectResult
,
k
:
string
)
=>
{
// a user owned project
if
(
v
.
from
.
users
.
length
>
0
)
{
// preserve the current checked state if the project already exists in this group
if
(
k
in
state
[
Group
.
PROJECTS
])
{
state
[
Group
.
PROJECTS
][
k
]
=
{
checked
:
state
[
Group
.
PROJECTS
][
k
].
checked
};
}
else
{
state
[
Group
.
PROJECTS
][
k
]
=
{
checked
:
true
};
}
}
else
if
(
v
.
from
.
selected
)
{
// a custom project
// preserve the current checked state if the project already exists in this group
if
(
k
in
state
[
Group
.
PROJECTS
])
{
state
[
Group
.
PROJECTS
][
k
]
=
{
checked
:
state
[
Group
.
PROJECTS
][
k
].
checked
,
custom
:
true
,
};
}
else
{
state
[
Group
.
PROJECTS
][
k
]
=
{
checked
:
true
,
custom
:
true
};
}
}
// Add each upstream.
v
.
upstreams
.
forEach
(
v
=>
{
state
[
Group
.
UPSTREAM
][
v
]
=
{
checked
:
false
};
state
.
projectData
[
v
]
=
{};
});
// collect upstreams for each project in the results
const
upstreamsDeps
=
v
.
project
.
dependencies
.
upstreams
;
// Add each downstream.
v
.
downstreams
.
forEach
(
v
=>
{
state
[
Group
.
DOWNSTREAM
][
v
]
=
{
checked
:
false
};
state
.
projectData
[
v
]
=
{};
});
// collect downstreams for each project in the results
const
downstreamsDeps
=
v
.
project
.
dependencies
.
downstreams
;
// Update project data for each.
});
// Add each upstream/downstream for the selected or user project
if
(
v
.
from
.
users
.
length
>
0
||
v
.
from
.
selected
)
{
_
.
forIn
(
upstreamsDeps
,
v
=>
{
v
.
id
.
forEach
(
v
=>
{
// preserve the current checked state if the project already exists in this group
if
(
v
in
state
[
Group
.
UPSTREAM
])
{
state
[
Group
.
UPSTREAM
][
v
]
=
{
checked
:
state
[
Group
.
UPSTREAM
][
v
].
checked
};
}
else
{
state
[
Group
.
UPSTREAM
][
v
]
=
{
checked
:
false
};
}
});
});
_
.
forIn
(
downstreamsDeps
,
v
=>
{
v
.
id
.
forEach
(
v
=>
{
// preserve the current checked state if the project already exists in this group
if
(
v
in
state
[
Group
.
DOWNSTREAM
])
{
state
[
Group
.
DOWNSTREAM
][
v
]
=
{
checked
:
state
[
Group
.
DOWNSTREAM
][
v
].
checked
};
}
else
{
state
[
Group
.
DOWNSTREAM
][
v
]
=
{
checked
:
false
};
}
});
});
}
// stores the raw project data for each project in the API result
state
.
projectData
[
k
]
=
{
name
:
v
.
project
.
name
,
tier
:
v
.
project
.
tier
,
owners
:
v
.
project
.
owners
,
languages
:
v
.
project
.
languages
,
data
:
v
.
project
.
data
,
dependencies
:
{
upstreams
:
upstreamsDeps
,
downstreams
:
downstreamsDeps
,
},
};
}
);
return
newPostAPICallState
;
}
case
"
HYDRATE_ERROR
"
:
/*
TODO: do we want to handle the error state differently? For example, when we render the error on the UI,
it won't disapper unless there's a successful API call or if the user refreshes the page. If a user performs other
actions, such as use the toggle/checkbox/ etc. the error message will be still be on the page
TODO: when we add error handling for projects not found, we'll need to make sure we remove the not-found-project from project group
(it's added automatically in the "ADD_PROJECTS" state)
*/
return
{
...
state
,
loading
:
false
,
error
:
action
.
payload
.
result
};
default
:
throw
new
Error
(
`unknown resolver action`
);
}
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment