-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Mirroring and Credential UI #838
base: new-webapp
Are you sure you want to change the base?
Conversation
Motivation: Creating and editing mirror settings require manual JSON editing which is time-consuming and error-prone. If a dedicated UI validates inputs and suggests default values such as mirrorable repositories and credentials, users will easily configure a mirror setting without making a mistake. Related: line#50 Modifications: - Server: - Added dedicated REST APIs for mirror configurations and credential information. - Creating, listing, and updating operaitons are supported. - An array index is used as a key to update the existing configuration. This is an inevitable choice since the existing settings stored in an array and JSON patch can update an element based on index. - Fixed `AddOperation` to create an JSON file if not exists. - Added `enabled` property to `MirrorCredential` so that users can disable unused credentials. - Added `id` property to `Mirror` so that users distinguish a mirror from others. - Removed support for multiple mirror that is currenly not used. line#647 - Annotated `@JsonProperties` to the getters of `MirrorCredential` and its subclasses to serialize `MirrorCredential` to JSON as a REST API response. - Webapp: - The following web pages were newly added for mirroring and credential. - Mirroring: - Create: `/app/projects/{projectName}/mirrors/new` - Update: `/app/projects/{projectName}/mirrors/{index}/edit` - List: `/app/projects/{projectName}/mirrors` - View: `/app/projects/{projectName}/mirrors/{index}` - Credential: - Create: `/app/projects/{projectName}/credentials/new` - Update: `/app/projects/{projectName}/credentials/{index}/edit` - List: `/app/projects/{projectName}/credentials` - View: `/app/projects/{projectName}/credentials/{index}` - Switched query-based tab URL for metadata to path-based to easily create breadcrumbs based on the path. - The old paths: `/app/projects/metadata/{projectName}?tab=repositories|permissions}members|tokens|mirror` - The new paths: - `/app/projects/{projectName}/metadata` - `/app/projects/{projectName}/permissions` - `/app/projects/{projectName}/permissions/repos/{repoName}` - `/app/projects/{projectName}/members` - `/app/projects/{projectName}/tokens` - `/app/projects/{projectName}/mirrors` - `/app/projects/{projectName}/credentials` - Updated `Breadcrumbs` to replace a specific path segment name with another to display more meaning information to breadcrumbs rather than showing an index number. Result: - You can now use a web UI to set up a Git-to-CD mirror. - Closes line#50
@Type(value = SingleMirrorConfig.class, name = "single"), | ||
@Type(value = MultipleMirrorConfig.class, name = "multiple") | ||
}) | ||
private abstract static class MirrorConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MultipleMirrorConfig
is removed and SingleMirrorConfig
is migrated to MirrorConfig.java
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd feel better if we migrate the internally used MultipleMirrorConfig
before merging this PR (or at least before the next release so that the next release driver isn't suprised)
// Next.js uses a path pattern to dynamically match the request path to a specific HTML file. | ||
// For example, `/app/projects/[projectNames]/index.html' is generated to render `/app/projects/myProj` | ||
// page. `FileService` serving the static resources for Central Dogma webapp does not understand the | ||
// path pattern syntax in the folder names. If `/app/projects/myProj` is requested, the `FileService` will | ||
// try to find '/app/projects/myProj/index.html' as is and fails, which in turn 'index.html' is returned as | ||
// a fallback. As a workaround, this triggers Next.js router to route to the desired page when a page is | ||
// loaded for the first time. | ||
if (router.asPath !== '/') { | ||
if (router.asPath !== '/' && router.isReady) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If router.isReady
is false, router.asPath
could include a path pattern such as [projectName]
. If router.isReady
becomes true, the path pattern is replaced with the actual value like myProj
.
if (path.toString().isEmpty()) { | ||
return valueCopy(); | ||
} | ||
if (node.isNull() && Strings.isNullOrEmpty(path.head().getMatchingProperty()) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) Is there no need to add such logic to other operations such as MoveOperation
, CopyOperation
for the sake of consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AddOperation
could be performed on an empty node to add a new value.
However, MoveOperation
and CopyOperation
need an old value to perform. So I think it is abnormal behavior to run copy
or move
operation on an empty value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, makes sense 👍
* Returns whether this {@link MirrorCredential} is enabled. | ||
*/ | ||
@JsonProperty("enabled") | ||
boolean enabled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double checked that enabled
is by default true
if the value is null
for both MirrorConfig
, MirrorCredential
// Web browsers may send an empty array `[]` as a value of hostnamePatterns | ||
// which is converted into `[null]`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) Is it possible for browsers to clean the input before sending such parameters? This feels unintuitive since the Web UI isn't the only code-path which uses this logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible. I can inject a custom logic to covert an empty array to null for `hostnamePatterns.
remoteRepoUri.getScheme(), | ||
remoteRepoUri.getHost() + remoteRepoUri.getPath(), | ||
mirror.remotePath(), | ||
firstNonNull(mirror.remoteBranch(), "master"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we need to fallback to master
. If I remember correctly, @trustin added a capability to detect the default branch by default if this value is null, and master
is no longer the de-facto if due to main
.
Is this necessary for the UI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can leave it as null
if a user specified null
(= use default branch).
* - remoteBranch: master | ||
* | ||
* <p>e.g. dogma://foo.com/bar/qux.dogma is split into: | ||
* - remoteRepoUri: dogma://foo.com/bar/qux.dogma | ||
* - remotePath: / (default) | ||
* - remoteBranch: {@code defaultBranch} | ||
*/ | ||
static String[] split(URI remoteUri, String suffix) { | ||
public static String[] splitRemoteUri(URI remoteUri, String suffix, @Nullable String defaultBranch) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like mentioned in another comment, I'm unsure why this change is necessary 😅 Would you mind explaining/updating in the PR description?
public static String[] splitRemoteUri(URI remoteUri, String suffix, @Nullable String defaultBranch) { | |
public static String[] splitRemoteUri(URI remoteUri, String suffix) { |
Please don't review the client part. The change is too huge that is hard to review. I will send a request to the main branch away. You can check the front-end code on the final PR if you want. |
// TODO(ikhoon): Automatically inject the non-optional query parameters to Route.matchesParam(). | ||
@MatchesParam("id") | ||
@Get("/projects/{projectName}/credentials") | ||
public CompletableFuture<MirrorCredential> getCredentialById(@Param String projectName, @Param String id) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) Are credentialId
s unique? I wasn't able to find any checks for duplicates, and I think users can still manually modify the credentials.json
file anyways
I wasn't able to check the UI, but I'm wondering what happens if
- a credential doesn't have an id
- a credential has a duplicate id
if (c == null) { | ||
throw new RepositoryMetadataException(PATH_MIRRORS + " contains null."); | ||
} | ||
final Mirror mirror = c.toMirror(parent(), credentials, index++); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) What do you think of just persistently assigning an id? I understand that previously we didn't have mirrors, but I think it's possible to migrate to an object based schema instead of a mirror based schema and enforce ids and create an arbitrary id if missing.
With the current setup, I fear that users might make a mistake if multiple users modify mirror for the same project. i.e. Two users may try to modify a mirror configuration concurrently, and the index mapping might change.
@Type(value = SingleMirrorConfig.class, name = "single"), | ||
@Type(value = MultipleMirrorConfig.class, name = "multiple") | ||
}) | ||
private abstract static class MirrorConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd feel better if we migrate the internally used MultipleMirrorConfig
before merging this PR (or at least before the next release so that the next release driver isn't suprised)
Minwoo also worked for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I couldn't find it, but do we also provide a way to delete a mirror/credential? Or is enabled
the equivalent of this?
final Map<String, Entry<?>> entries = | ||
find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).join(); | ||
private CompletableFuture<Map.Entry<List<Mirror>, List<MirrorCredential>>> mirrorsAndCredentials(int rev) { | ||
return find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).thenApply( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).thenApply( | |
return find(new Revision(rev, PATH_CREDENTIALS_AND_MIRRORS).thenApply( |
I don't think we can provide an API for removal because of the limitation of JSON patch that can update an array with its index. An index for an item should be consistent to provide a deep link for a mirror view and edit. Additionally, the size of Git mirrors and credentials isn't growing up to 100 or 1000. The As a following work, we will hide If removal for the mirror and credential information is really required, we can choose one of the following:
As you know, key / ID fields for |
Agreed. Maybe we could auto-generate IDs and commit the changes automatically when it's read first time? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I guess I didn't read the PR description carefully and didn't catch the intention 😅
I think there should be no problem with the current approach if the mirrors.json
, credentials.json
is not editable directly by end-users as a short-term solution 👍
I'm unsure how this will be rolled out with respect to the UI migration though (at the same time? vs. after UI migration is complete?)
throw new EntryNotFoundException( | ||
"No such mirror at the index " + index + " in " + projectName); | ||
} | ||
return convertToMirrorDto(projectName, mirrors.get(index)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we're just using the index and not actually comparing mirrors.index
with index
from the parameters.
What do you think of just removing the index from the DTO altogether for simplicity if we know it's not going to change?
I didn't think there is any sorting from any point (since this is an array of json objects, there probably isn't a natural sorting order anyways)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about changing the directory layout of mirrors and credentials? e.g.
- mirrors
- <mirror-id>.json
- ...
- credentials
- <credentials-id>.json
- ...
At startup, we could:
- perform automatic migration from
mirrors.json
andcredentials.json
to the new layout; and - rename
mirrors.json
andcredentials.json
tomirrors.json.bak
andcredentials.json.bak
(so that the migration doesn't repeat.)
Then, all mirrors and credentials will have their unique IDs and we don't need to use integral index which is confusing.
I think this can be done as a follow-up task, though. (or we can do the migration first?) What do you think?
registerModules(new Jdk8Module(), | ||
new SimpleModule().addSerializer(Instant.class, InstantSerializer.INSTANCE) | ||
.addDeserializer(Instant.class, InstantDeserializer.INSTANT)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not related to this PR, but should we do this in Armeria's DefaultJacksonObjectMapperProvider
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Armeria's default object mapper uses SPI to load all available modules. The new Java 8 API and time API are declared as dependencies.
https://github.com/line/armeria/blob/219ee3758b232ca9c47e03258a0a62f4cc4841c3/core/src/main/java/com/linecorp/armeria/internal/common/DefaultJacksonObjectMapperProvider.java#L42-L43
https://github.com/line/armeria/blob/70296b99b4277eef75da93ec85eeb26beec8d39e/core/build.gradle#L107-L108
@ProducesJson | ||
@RequiresRole(roles = ProjectRole.OWNER) | ||
@ExceptionHandler(HttpApiExceptionHandler.class) | ||
public class CredentialServiceV1 extends AbstractService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public class CredentialServiceV1 extends AbstractService { | |
public final class CredentialServiceV1 extends AbstractService { |
remoteRepoUri.getScheme(), | ||
remoteRepoUri.getHost() + remoteRepoUri.getPath(), | ||
mirror.remotePath(), | ||
firstNonNull(mirror.remoteBranch(), "master"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can leave it as null
if a user specified null
(= use default branch).
return metadataRepo.push(projectName, Project.REPO_META, author, summary, change); | ||
} | ||
|
||
private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) { | |
private static MirrorConfig convertToMirrorConfig(MirrorDto mirrorDto) { |
private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) { | ||
final String remoteUri = | ||
mirrorDto.remoteScheme() + "://" + mirrorDto.remoteUrl() + | ||
MirrorUtil.normalizePath(mirrorDto.remotePath()) + '#' + mirrorDto.remoteBranch(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#branch
should be omitted if remoteBranch
is null
The design looks good. One thing to consider is mirror ID or credential ID are optional fields. To adopt this idea, some random IDs should be used to fill the null IDs I will work on the migration first before merging this PR. The change of this is already huge. It would be easier to review if it is handled separately in advance. |
Motivation:
Creating and editing mirror settings require manual JSON editing which is time-consuming and error-prone. If a dedicated UI validates inputs and suggests default values such as mirrorable repositories and credentials, users will easily configure a mirror setting without making a mistake.
Related: #50
Modifications:
AddOperation
to create an JSON file if not exists.enabled
property toMirrorCredential
so that users can disable unused credentials.id
property toMirror
so that users distinguish a mirror from others.@JsonProperties
to the getters ofMirrorCredential
and its subclasses to serializeMirrorCredential
to JSON as a REST API response./app/projects/{projectName}/mirrors/new
/app/projects/{projectName}/mirrors/{index}/edit
/app/projects/{projectName}/mirrors
/app/projects/{projectName}/mirrors/{index}
/app/projects/{projectName}/credentials/new
/app/projects/{projectName}/credentials/{index}/edit
/app/projects/{projectName}/credentials
/app/projects/{projectName}/credentials/{index}
/app/projects/metadata/{projectName}?tab=repositories|permissions}members|tokens|mirror
/app/projects/{projectName}/metadata
/app/projects/{projectName}/permissions
-/app/projects/{projectName}/permissions/repos/{repoName}
-/app/projects/{projectName}/members
-/app/projects/{projectName}/tokens
-/app/projects/{projectName}/mirrors
-/app/projects/{projectName}/credentials
Breadcrumbs
to replace a specific path segment name with another to display more meaningful information for breadcrumbs rather than showing an index number.Result: