Skip to content

Allow editing of files via web application #466

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

Merged
merged 11 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- New UI for the basic mode Sync (#415)
- Allow changing namespaces and IPM package context from web UI (#280)
- Support for editing repo from filesystem perspective via web application (#464)
- Support for downloading a VSCode workspace file from web UI

### Fixed
- Instance wide settings are placed in proper global (#444)
- Avoid delay/errors in loading interop JS when there is a URL prefix (e.g., instance name in multi-instance webserver configuration)
- Added proper JS escaping in sync output
- Added support to switch branch in basic mode from menu (#451)

## [2.4.1] - 2024-08-02
Expand Down
29 changes: 20 additions & 9 deletions cls/SourceControl/Git/Extension.cls
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ Method AfterUserAction(Type As %Integer, Name As %String, InternalName As %Strin
if menu '= "%SourceMenu", menu'="%SourceContext" {
quit $$$OK
}
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName, .fromWebApp)
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
set ec = ##class(SourceControl.Git.Utils).AfterUserAction(Type, Name, InternalName, .Answer, .Msg, .Reload)
if fromWebApp {
// Force reload and compile of actual item if underlying file has changed
do ..OnBeforeLoad(InternalName,1,1)
}
quit ec
}

Expand Down Expand Up @@ -235,12 +239,12 @@ Method OnMenuItem(MenuName As %String, InternalName As %String, SelectedText As

/// This is called before the actual load of data to give the chance
/// to load the item from an external format.
Method OnBeforeLoad(InternalName As %String, verbose As %Boolean) As %Status
Method OnBeforeLoad(InternalName As %String, verbose As %Boolean, compile As %Boolean = 0) As %Status
{
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) {
quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0)
quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0,compile)
}
quit $$$OK
}
Expand Down Expand Up @@ -274,14 +278,21 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU
{
set sc = $$$OK
try {
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName)
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName,.fromWebApp,.fullExternalName)
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) {
set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
$$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
$$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
if fromWebApp {
if fullExternalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) {
// Reimport item into database
$$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(InternalName,,1,1))
}
} else {
set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
$$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
$$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
}
}
}
} catch e {
Expand Down
74 changes: 74 additions & 0 deletions cls/SourceControl/Git/Settings.cls
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Property defaultMergeBranch As %String [ InitialExpression = {##class(SourceCont
/// Compile using the configured pull event handler when "Import All" is run
Property compileOnImport As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).CompileOnImport()} ];

/// Define a namespace-level web application allowing access to multiple git repos across separate namespaces
Property namespaceLevelGitWebApp As %Boolean [ InitialExpression = {##class(SourceControl.Git.Settings).HasNamespaceWebApp()} ];

Property Mappings [ MultiDimensional ];

Method %OnNew() As %Status
Expand Down Expand Up @@ -160,6 +163,75 @@ ClassMethod Configure() As %Boolean [ CodeMode = objectgenerator ]
do %code.WriteLine(" quit 1")
}

Method ConfigureNamespaceWebApplication()
{
Set root = ##class(%Library.File).NormalizeDirectory(##class(SourceControl.Git.Utils).TempFolder())
Set deleteWebApp = ..HasNamespaceWebApp(.appDirectory) && '..namespaceLevelGitWebApp
Set createWebApp = ..namespaceLevelGitWebApp && '..HasNamespaceWebApp()
Do ..WebAppOperation("/git/"_$Namespace_"/", createWebApp, deleteWebApp, root)
}

Method WebAppOperation(name, create As %Boolean, delete As %Boolean, root As %String) [ Private ]
{
Set namespace = $Namespace
New $Namespace
Set $Namespace = "%SYS"
If $Extract(name) = "/" {
Set name = $Extract(name,1,*-1)
}
If delete {
If ##class(Security.Applications).Exists(name) {
$$$ThrowOnError(##class(Security.Applications).Delete(name))
Write !,"Removed web application "_name
}
Quit
}

// These are the only things we want to coerce.
Set props("AutheEnabled")=0 // No auth methods enabled = impossible to use
Set props("InbndWebServicesEnabled")=0
Set props("ServeFiles")=0
Set props("Enabled")=1
Set props("Name")=name
Set props("NameSpace")=namespace
Set props("Path")=root
Set props("Type")=2
Set props("Recurse")=1
If create {
Write !,"Creating web application: "_name_"... "
$$$ThrowOnError(##class(Security.Applications).Create(name,.props))
Write "done."
} ElseIf ##class(Security.Applications).Exists(name) {
Write !,"Web application '"_name_"' already exists."
$$$ThrowOnError(##class(Security.Applications).Get(name,.existingProps))
Set changes = 0
Set key = ""
For {
Set key = $Order(props(key),1,value)
Quit:key=""
If (value '= $Get(existingProps(key))) {
Write !,"Changing "_key_": "_$Get(existingProps(key))_" -> "_value
Set changes = 1
}
}
If changes {
$$$ThrowOnError(##class(Security.Applications).Modify(name,.props))
Write !,"Web application '"_name_"' updated."
} Else {
Write !,"No changes made to web application."
}
}
}

ClassMethod HasNamespaceWebApp(Output webAppDirectory) As %Boolean
{
Set webAppDirectory = $System.CSP.GetFileName("/git/"_$Namespace_"/")
If (webAppDirectory '= "") {
Set webAppDirectory = ##class(%Library.File).NormalizeDirectory(webAppDirectory)
}
Quit (webAppDirectory '= "")
}

Method OnAfterConfigure() As %Boolean
{
set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask
Expand All @@ -186,6 +258,8 @@ Method OnAfterConfigure() As %Boolean
}
}

do ..ConfigureNamespaceWebApplication()

set gitDir = ##class(%File).NormalizeDirectory(..namespaceTemp)_".git"
if '##class(%File).DirectoryExists(gitDir) {
set list(1) = "Initialize empty repo"
Expand Down
3 changes: 1 addition & 2 deletions cls/SourceControl/Git/StreamServer.cls
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Class SourceControl.Git.StreamServer Extends %CSP.StreamServer
ClassMethod OnPage() As %Status
{
if (%stream '= $$$NULLOREF) && $data(%base)#2 {
set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude()
set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude(%request.URLPrefix)
while '%stream.AtEnd {
set text = %stream.Read()
set text = $replace(text,"{{baseHref}}",..EscapeHTML(%base))
Expand All @@ -19,4 +19,3 @@ ClassMethod OnPage() As %Status
}

}

21 changes: 15 additions & 6 deletions cls/SourceControl/Git/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,7 @@ ClassMethod FullExternalName(ByRef InternalName As %String, ByRef MappingExists
..TempFolder()_..ExternalName(.InternalName, .MappingExists)
}

ClassMethod NormalizeInternalName(ByRef name As %String) As %String
ClassMethod NormalizeInternalName(ByRef name As %String, Output fromWebApp As %Boolean = 0, Output cspFilename) As %String
{
//Studio passes name of routine with dots as it is in folders
//e.g. Package.SubPackage.Routine.mac has InternalName = /Package/SubPackage/Routine.mac
Expand All @@ -1132,6 +1132,16 @@ ClassMethod NormalizeInternalName(ByRef name As %String) As %String
if ($extract(name) '= "/") && (type'="csp") {
quit $piece(name,".",1,*-1)_"."_$zconvert($piece(name,".",*),"U")
}

if (name [ "/") && (type = "csp") {
set cspFilename = $System.CSP.GetFileName(name)
if (cspFilename '= "") && (cspFilename [ ..TempFolder()) {
set name = ..NameToInternalName(cspFilename)
set fromWebApp = 1
} else {
kill cspFilename
}
}

if (type = "inc") || (type = "mac") || (type = "int") {
set name = $extract($translate(name, "/", "."), 2, *)
Expand Down Expand Up @@ -1246,7 +1256,7 @@ ClassMethod FixProjectCspReferences(projectName As %String) As %Status

/// imports file if version in system is newer then version on disk.
/// if <var>force</var> = 1 then imports in any case.
ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1) As %Status
ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1, compile As %Boolean = 0) As %Status
{
#dim filename as %String = ..FullExternalName(.InternalName)
#dim fileTSH = ##class(%File).GetFileDateModified(filename)
Expand All @@ -1265,7 +1275,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A
if ($extract(InternalName, 1) = "/"){
set sc = ..ImportCSPFile(InternalName)
} else{
set sc = $system.OBJ.Load(filename,"-l-d")
set sc = $system.OBJ.Load(filename,$Select(compile:"ck-l",1:"-l-d"))
}
}
if sc {
Expand Down Expand Up @@ -2396,10 +2406,10 @@ ClassMethod GetPackageVersion() As %String [ CodeMode = objectgenerator ]
quit $$$OK
}

ClassMethod GetSourceControlInclude() As %String
ClassMethod GetSourceControlInclude(prefix As %String = "") As %String
{
quit $select(##class(%Library.EnsembleMgr).IsEnsembleInstalled():
"<script type=""text/javascript"" src=""/isc/studio/templates/ensemble/Ens_SourceControl.js""></script>",
"<script type=""text/javascript"" src="""_prefix_"/isc/studio/templates/ensemble/Ens_SourceControl.js""></script>",
1: "")
}

Expand Down Expand Up @@ -2615,4 +2625,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status
}

}

59 changes: 59 additions & 0 deletions cls/SourceControl/Git/WebUIDriver.cls
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,65 @@ ClassMethod HandleRequest(pagePath As %String, InternalName As %String = "", Out
if $isobject($get(responseJSON)) {
do responseJSON.%ToJSON(%data)
}
} elseif pathStart = "vscode-workspace" {
set handled = 1
set namespace = $Namespace
set instanceName = $ZConvert(##class(%SYS.System).InstanceGUID(),"L")
set hasCSP = 0
if context.IsInGitEnabledPackage && 'context.IsInDefaultPackage {
set workspaceFilename = context.Package.Name_"-"_instanceName_".code-workspace"
// Loading an IPM package in dev mode automatically creates a project for it, so filter to that.
set filter = "?project="_context.Package.Name
} else {
set hasCSP = ##class(SourceControl.Git.Settings).HasNamespaceWebApp()
set workspaceFilename = namespace_"-"_instanceName_".code-workspace"
set filter = "?mapped=0"
}
set server = {
"webServer": {
"host": (%request.CgiEnvs("SERVER_NAME")),
"port": (+%request.CgiEnvs("SERVER_PORT")),
"scheme": ($Select(%request.Secure:"https",1:"http"))
},
"username": ($Username)
}
if (%request.URLPrefix '= "") {
set server.webServer.pathPrefix = %request.URLPrefix
}
set workspaceDef = {
"folders": [
],
"settings": {
"intersystems.servers": {
"/default": (instanceName)
},
"objectscript.conn": {
"active": false
}
}
}
if hasCSP {
do workspaceDef.folders.%Push({
"name": ($Namespace),
"uri": ("isfs://"_instanceName_":"_$Namespace_"/git/"_$Namespace_"?csp")
})
} else {
do workspaceDef.folders.%Push({
"name": ($Namespace),
"uri": ("isfs://"_instanceName_":"_$Namespace_"/"_filter)
})
}
set $Property(workspaceDef.settings."intersystems.servers",instanceName) = server
set stream = ##class(%CSP.CharacterStream).%New()
do stream.SetAttribute("ContentDisposition","attachment; filename="_workspaceFilename)
set formatter = ##class(%JSON.Formatter).%New()
do formatter.FormatToStream(workspaceDef,stream)
$$$ThrowOnError(stream.%Save())
do %request.Set("STREAMOID",##class(%CSP.Page).Encrypt(stream.%Oid()))
set %request.Data("EXPIRES",1,"encrypt") = 1
do %request.Set("EXPIRES",0)
do ##class(%CSP.StreamServer).OnPreHTTP() // Need to call this to set headers properly
set %stream = 1 // Leak this to webuidriver.csp
} elseif $match(pathStart,"git-command|git|dirname|hostname|viewonly|contexts") {
if (%request.Method = "GET") {
set %data = ##class(%Stream.TmpCharacter).%New()
Expand Down
2 changes: 1 addition & 1 deletion csp/sync.csp
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@

&js<
var outputContainer = document.getElementById('outputBox');
var lineText = "#(escapedLine)#";
var lineText = #(..QuoteJS(escapedLine))#;
var lineTextNode = document.createTextNode(lineText);
outputContainer.innerHTML += lineText + "<br>";
>
Expand Down
29 changes: 23 additions & 6 deletions git-webui/release/share/git-webui/webui/css/git-webui.css
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ body {
padding-bottom: 100px;
}
#sidebar #sidebar-content > :first-child,
#sidebar #sidebar-content > :nth-last-child(2) {
#sidebar #sidebar-content > :nth-last-child(3) {
border-top: 1px solid #5e5e5e;
}
#sidebar #sidebar-content h4:before {
Expand Down Expand Up @@ -216,26 +216,43 @@ body {
#sidebar #sidebar-content #sidebar-tags h4:before {
content: url(../img/tag.svg);
}
#sidebar #sidebar-content #sidebar-vscode a {
color: white;
}
#sidebar #sidebar-content #sidebar-vscode h4 {
padding: 0px;
}
#sidebar #sidebar-content #sidebar-vscode h4:before {
content: url(../img/file.svg);
}
#sidebar #sidebar-content #sidebar-vscode {
position: absolute;
bottom: 80px;
width: 16.7em;
background-color: #333333;
}
#sidebar #sidebar-content #sidebar-context h4 {
padding: 0px;
}
#sidebar #sidebar-content #sidebar-context h4:before {
content: url(../img/context.svg);
}
#sidebar #sidebar-content #sidebar-context {
position: absolute;
bottom: 50px;
bottom: 40px;
width: 16.7em;
padding-bottom: 0.3rem;
margin-bottom: 0;
background-color: #333333;
}
#sidebar #sidebar-content #sidebar-settings h4 {
padding: 0px;
}
#sidebar #sidebar-content #sidebar-settings h4:before {
content: url(../img/gear-fill.svg);
}
#sidebar #sidebar-content #sidebar-settings {
position: absolute;
bottom: 0px;
width: 16.7em;
padding-bottom: 0.3rem;
margin-bottom: 0px;
background-color: #333333;
}
#sidebar #sidebar-content ul {
Expand Down
2 changes: 1 addition & 1 deletion git-webui/release/share/git-webui/webui/img/context.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading