FELIX-1261: Install/uninstall features from web console
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@790920 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/webconsole/pom.xml b/karaf/webconsole/pom.xml
index 432c9ef..857fadf 100644
--- a/karaf/webconsole/pom.xml
+++ b/karaf/webconsole/pom.xml
@@ -32,7 +32,7 @@
<artifactId>org.apache.felix.karaf.webconsole</artifactId>
<packaging>bundle</packaging>
<version>1.2.0-SNAPSHOT</version>
- <name>Apache Felix Karaf :: Web Console</name>
+ <name>Apache Felix Karaf :: Web Console Features Plugin</name>
<dependencies>
<dependency>
@@ -56,6 +56,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>org.apache.felix.karaf.gshell</groupId>
<artifactId>org.apache.felix.karaf.gshell.features</artifactId>
</dependency>
@@ -64,6 +69,13 @@
<artifactId>org.apache.servicemix.bundles.junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.json</groupId>
+ <artifactId>json</artifactId>
+ <version>20070829</version>
+ <scope>compile</scope>
+ <optional>true</optional>
+ </dependency>
</dependencies>
<build>
@@ -73,7 +85,12 @@
<artifactId>maven-bundle-plugin</artifactId>
<configuration>
<instructions>
- <Private-Package>org.apache.felix.karaf.webconsole*</Private-Package>
+ <Export-Package>org.apache.felix.karaf.webconsole;version=${pom.version}</Export-Package>
+ <Embed-Dependency>
+ <!-- Required for JSON data transfer -->
+ <!-- TODO: this needs to be put in a common place for reuse. -->
+ json
+ </Embed-Dependency>
</instructions>
</configuration>
</plugin>
diff --git a/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/Feature.java b/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/Feature.java
new file mode 100644
index 0000000..cc2a627
--- /dev/null
+++ b/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/Feature.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.webconsole;
+
+/**
+ * Represents a feature with a name, version and state
+ */
+public class Feature {
+
+ public enum State {
+ INSTALLED, UNINSTALLED, UNKNOWN;
+
+ @Override
+ public String toString() {
+ //only capitalize the first letter
+ String s = super.toString();
+ return s.substring( 0, 1 ) + s.substring( 1 ).toLowerCase();
+ }
+ };
+
+ protected String name;
+
+ protected String version;
+
+ protected State state;
+
+
+ public Feature(String name, String version, State state) {
+ this.name = name;
+ this.version = version;
+ this.state = state;
+ }
+}
diff --git a/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java b/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java
index 3351220..ad22929 100644
--- a/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java
+++ b/karaf/webconsole/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java
@@ -16,83 +16,538 @@
*/
package org.apache.felix.karaf.webconsole;
+
import java.io.IOException;
+import java.io.InputStream;
import java.io.PrintWriter;
-import javax.servlet.Servlet;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Comparator;
+
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.apache.felix.karaf.gshell.features.FeaturesService;
+import org.apache.felix.karaf.gshell.features.Repository;
import org.apache.felix.webconsole.AbstractWebConsolePlugin;
-/**
- * Felix Web Console plugin to interact with Karaf features
- *
- * @author Marcin Wilkos
- */
-public class FeaturesPlugin extends AbstractWebConsolePlugin implements Servlet {
+import org.json.JSONException;
+import org.json.JSONWriter;
- private static final String LABEL = "features";
- private static final String TITLE = "Features";
-
+import org.osgi.framework.BundleContext;
+
+
+/**
+ * The <code>FeaturesPlugin</code>
+ */
+public class FeaturesPlugin extends AbstractWebConsolePlugin
+{
+
+ /** Pseudo class version ID to keep the IDE quite. */
+ private static final long serialVersionUID = 1L;
+
+ public static final String NAME = "features";
+
+ public static final String LABEL = "Features";
+
+ private Log log = LogFactory.getLog(FeaturesPlugin.class);
+
+ private ClassLoader classLoader;
+
+ private String featuresJs = "/features/res/ui/features.js";
+
private FeaturesService featuresService;
- public FeaturesPlugin() {
- super();
+ private BundleContext bundleContext;
+
+
+ /*
+ * Blueprint lifecycle callback methods
+ */
+
+ public void start()
+ {
+ super.activate( bundleContext );
+
+ this.classLoader = this.getClass().getClassLoader();
+
+ this.log.info( LABEL + " plugin activated" );
}
- public String getTitle() {
- return TITLE;
+ public void stop()
+ {
+ this.log.info( LABEL + " plugin deactivated" );
+ super.deactivate();
}
- public String getLabel() {
+ //
+ // AbstractWebConsolePlugin interface
+ //
+ public String getLabel()
+ {
+ return NAME;
+ }
+
+
+ public String getTitle()
+ {
return LABEL;
}
- protected void renderContent(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- PrintWriter pw = response.getWriter();
- pw.println("<pre>");
- pw.println("</pre>");
- pw.println( "<table class='content' cellpadding='0' cellspacing='0' width='100%'>" );
+ protected void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException
+ {
+ boolean success = false;
- pw.println( "<tr class='content'>" );
- pw.println( "<th class='content container'>" + getTitle() + "</th>" );
- pw.println( "</tr>" );
+ final String action = req.getParameter( "action" );
+ final String feature = req.getParameter( "feature" );
+ final String version = req.getParameter( "version" );
+ final String url = req.getParameter( "url" );
- pw.println( "<tr class='content'>" );
- pw.println( "<td class='content'>" );
- pw.println( "<pre>" );
-
- pw.println("*** Features:");
- String[] features;
- try {
- features = getFeatures();
- } catch (Exception e) {
- throw new ServletException("Unable to fetch features", e);
+ if ( action == null )
+ {
+ success = true;
}
- for(int i=0; i<features.length;i++){
- pw.println(features[i]);
+ else if ( "installFeature".equals( action ) )
+ {
+ success = this.installFeature(feature, version);
+ }
+ else if ( "uninstallFeature".equals( action ) )
+ {
+ success = this.uninstallFeature( feature, version );
+ }
+ else if ( "refreshRepository".equals( action ) )
+ {
+ success = this.refreshRepository( url );
+ }
+ else if ( "removeRepository".equals( action ) )
+ {
+ success = this.removeRepository( url );
+ }
+ else if ( "addRepository".equals( action ) )
+ {
+ success = this.addRepository( url );
}
- pw.println( "</pre>" );
- pw.println( "</td>" );
- pw.println( "</tr>" );
- pw.println( "</table>" );
+ if ( success )
+ {
+ // let's wait a little bit to give the framework time
+ // to process our request
+ try
+ {
+ Thread.sleep( 800 );
+ }
+ catch ( InterruptedException e )
+ {
+ // we ignore this
+ }
+ this.renderJSON( resp, null );
+ }
+ else
+ {
+ super.doPost( req, resp );
+ }
}
- /*
- * Get the list of installed/uninstalled features
- */
- private String[] getFeatures() throws Exception {
- return getFeaturesService().listFeatures();
+
+ protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException
+ {
+
+ // get request info from request attribute
+ final PrintWriter pw = response.getWriter();
+
+ String appRoot = ( String ) request
+ .getAttribute( "org.apache.felix.webconsole.internal.servlet.OsgiManager.appRoot" );
+ final String featuresScriptTag = "<script src='" + appRoot + this.featuresJs
+ + "' language='JavaScript'></script>";
+ pw.println( featuresScriptTag );
+
+ pw.println( "<script type='text/javascript'>" );
+ pw.println( "// <![CDATA[" );
+ pw.println( "var imgRoot = '" + appRoot + "/res/imgs';" );
+ pw.println( "// ]]>" );
+ pw.println( "</script>" );
+
+ pw.println( "<div id='plugin_content'/>" );
+
+ pw.println( "<script type='text/javascript'>" );
+ pw.println( "// <![CDATA[" );
+ pw.print( "renderFeatures( " );
+ writeJSON( pw );
+ pw.println( " )" );
+ pw.println( "// ]]>" );
+ pw.println( "</script>" );
+ }
+
+
+ //
+ // Additional methods
+ //
+
+ protected URL getResource( String path )
+ {
+ path = path.substring( NAME.length() + 1 );
+ URL url = this.classLoader.getResource( path );
+ try
+ {
+ InputStream ins = url.openStream();
+ if ( ins == null )
+ {
+ this.log.error( "failed to open " + url );
+ }
+ }
+ catch ( IOException e )
+ {
+ this.log.error( e.getMessage(), e );
+ }
+ return url;
+ }
+
+
+ private boolean installFeature(String feature, String version) {
+ boolean success = false;
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ }
+ try
+ {
+ featuresService.installFeature( feature, version );
+ success = true;
+ }
+ catch ( Exception e )
+ {
+ this.log.error( "failed to install feature: ", e );
+ }
+ return success;
+ }
+
+
+ private boolean uninstallFeature(String feature, String version) {
+ boolean success = false;
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ }
+ try
+ {
+ featuresService.uninstallFeature( feature, version );
+ success = true;
+ }
+ catch ( Exception e )
+ {
+ this.log.error( "failed to install feature: ", e );
+ }
+ return success;
+ }
+
+
+ private boolean removeRepository(String url) {
+ boolean success = false;
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ }
+ try
+ {
+ featuresService.removeRepository( new URI( url ) );
+ success = true;
+ }
+ catch ( Exception e )
+ {
+ this.log.error( "failed to install feature: ", e );
+ }
+ return success;
+ }
+
+
+ private boolean refreshRepository(String url) {
+ boolean success = false;
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ }
+ try
+ {
+ featuresService.removeRepository( new URI( url ) );
+ featuresService.addRepository( new URI( url ) );
+ success = true;
+ }
+ catch ( Exception e )
+ {
+ this.log.error( "failed to install feature: ", e );
+ }
+ return success;
+ }
+
+
+ private boolean addRepository(String url) {
+ boolean success = false;
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ }
+ try
+ {
+ featuresService.addRepository( new URI( url ) );
+ success = true;
+ }
+ catch ( Exception e )
+ {
+ this.log.error( "failed to install feature: ", e );
+ }
+ return success;
+ }
+
+
+ private void renderJSON( final HttpServletResponse response, final String feature ) throws IOException
+ {
+ response.setContentType( "application/json" );
+ response.setCharacterEncoding( "UTF-8" );
+
+ final PrintWriter pw = response.getWriter();
+ writeJSON( pw );
+ }
+
+
+ private void writeJSON( final PrintWriter pw ) throws IOException
+ {
+ final Feature[] features = this.getFeatures();
+ final String statusLine = this.getStatusLine( features );
+ final String[] repositories = this.getRepositories();
+
+ final JSONWriter jw = new JSONWriter( pw );
+
+ try
+ {
+ jw.object();
+
+ jw.key( "status" );
+ jw.value( statusLine );
+
+ jw.key( "features" );
+ jw.array();
+ for ( int i = 0; i < features.length; i++ )
+ {
+ featureInfo( jw, features[i] );
+ }
+ jw.endArray();
+
+ jw.key( "repositories" );
+ jw.array();
+ for ( int i = 0; i < repositories.length; i++ )
+ {
+ jw.object();
+ jw.key( "url" );
+ jw.value( repositories[i] );
+ jw.key( "actions" );
+ jw.array();
+ action( jw, true, "refreshRepository", "Refresh", "refresh" );
+ action( jw, true, "removeRepository", "Uninstall", "delete" );
+ jw.endArray();
+ jw.endObject();
+ }
+ jw.endArray();
+
+ jw.endObject();
+
+ }
+ catch ( JSONException je )
+ {
+ throw new IOException( je.toString() );
+ }
+
+ }
+
+
+ private String[] getRepositories()
+ {
+ String[] repositories = new String[0];
+
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ return repositories;
+ }
+
+ Repository[] repositoryInfo = null;
+ try
+ {
+ repositoryInfo = featuresService.listRepositories();
+ }
+ catch ( Exception e )
+ {
+ this.log.error( e.getMessage() );
+ return new String[0];
+ }
+
+ repositories = new String[repositoryInfo.length];
+ for ( int i = 0; i < repositoryInfo.length; i++ )
+ {
+ repositories[i] = repositoryInfo[i].getURI().toString();
+ }
+ return repositories;
+ }
+
+
+ private Feature[] getFeatures()
+ {
+ Feature[] features = new Feature[0];
+
+ if ( featuresService == null )
+ {
+ this.log.error( "GShell Features service is unavailable." );
+ return features;
+ }
+
+ String[] featureInfo = null;
+ try
+ {
+ featureInfo = featuresService.listFeatures();
+ }
+ catch ( Exception e )
+ {
+ this.log.error( e.getMessage() );
+ return new Feature[0];
+ }
+
+ features = new Feature[featureInfo.length];
+ for ( int i = 0; i < featureInfo.length; i++ )
+ {
+ String[] temp;
+ temp = getBracketedToken( featureInfo[i], 0 );
+ Feature.State state;
+ if ( "installed ".equals( temp[0] ) )
+ {
+ state = Feature.State.INSTALLED;
+ }
+ else if ( "uninstalled".equals( temp[0] ) )
+ {
+ state = Feature.State.UNINSTALLED;
+ }
+ else
+ {
+ state = Feature.State.UNKNOWN;
+ }
+ temp = getBracketedToken( temp[1], 0 );
+ String version = temp[0];
+ features[i] = new Feature( temp[1].trim(), version, state );
+ }
+ Arrays.sort( features, new FeatureComparator() );
+ return features;
+ }
+
+ private String[] getBracketedToken( String str, int startIndex )
+ {
+ int start = str.indexOf( '[', startIndex ) + 1;
+ int end = str.indexOf( ']', start );
+ String token = str.substring( start, end );
+ String remainder = str.substring( end + 1 );
+ return new String[]
+ { token, remainder };
+ }
+
+
+ class FeatureComparator implements Comparator<Feature>
+ {
+ public int compare( Feature o1, Feature o2 )
+ {
+ return o1.name.toLowerCase().compareTo( o2.name.toLowerCase() );
+ }
+ }
+
+
+ private String getStatusLine( final Feature[] features )
+ {
+ int installed = 0;
+ for ( int i = 0; i < features.length; i++ )
+ {
+ if ( features[i].state == Feature.State.INSTALLED )
+ {
+ installed++;
+ }
+ }
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append( "Feature information: " );
+ appendFeatureInfoCount( buffer, "in total", features.length );
+ if ( installed == features.length )
+ {
+ buffer.append( " - all " );
+ appendFeatureInfoCount( buffer, "active.", features.length );
+ }
+ else
+ {
+ if ( installed != 0 )
+ {
+ buffer.append( ", " );
+ appendFeatureInfoCount( buffer, "installed", installed );
+ }
+ buffer.append( '.' );
+ }
+ return buffer.toString();
+ }
+
+
+ private void appendFeatureInfoCount( final StringBuffer buf, String msg, int count )
+ {
+ buf.append( count );
+ buf.append( " feature" );
+ if ( count != 1 )
+ buf.append( 's' );
+ buf.append( ' ' );
+ buf.append( msg );
+ }
+
+
+ private void featureInfo( JSONWriter jw, Feature feature ) throws JSONException
+ {
+ jw.object();
+ jw.key( "name" );
+ jw.value( feature.name );
+ jw.key( "version" );
+ jw.value( feature.version );
+ jw.key( "state" );
+ jw.value( feature.state );
+
+ jw.key( "actions" );
+ jw.array();
+
+ if ( feature.state == Feature.State.INSTALLED )
+ {
+ action( jw, true, "uninstallFeature", "Uninstall", "delete" );
+ }
+ else
+ {
+ action( jw, true, "installFeature", "Install", "start" );
+ }
+ jw.endArray();
+
+ jw.endObject();
+ }
+
+
+ private void action( JSONWriter jw, boolean enabled, String op, String title, String image ) throws JSONException
+ {
+ jw.object();
+ jw.key( "enabled" ).value( enabled );
+ jw.key( "op" ).value( op );
+ jw.key( "title" ).value( title );
+ jw.key( "image" ).value( image );
+ jw.endObject();
}
- public FeaturesService getFeaturesService() {
- return featuresService;
- }
-
- public void setFeaturesService(FeaturesService featuresService) {
+ // DI setters
+ public void setFeaturesService(FeaturesService featuresService)
+ {
this.featuresService = featuresService;
}
+
+ public void setBundleContext(BundleContext bundleContext)
+ {
+ this.bundleContext = bundleContext;
+ }
}
diff --git a/karaf/webconsole/src/main/resources/OSGI-INF/blueprint/webconsole.xml b/karaf/webconsole/src/main/resources/OSGI-INF/blueprint/webconsole.xml
index 7f3d106..b564a35 100644
--- a/karaf/webconsole/src/main/resources/OSGI-INF/blueprint/webconsole.xml
+++ b/karaf/webconsole/src/main/resources/OSGI-INF/blueprint/webconsole.xml
@@ -22,8 +22,9 @@
<reference id="featuresService" interface="org.apache.felix.karaf.gshell.features.FeaturesService" />
- <bean id="featuresPlugin" class="org.apache.felix.karaf.webconsole.FeaturesPlugin">
+ <bean id="featuresPlugin" class="org.apache.felix.karaf.webconsole.FeaturesPlugin" init-method="start" destroy-method="stop">
<property name="featuresService" ref="featuresService" />
+ <property name="bundleContext" ref="blueprintBundleContext" />
</bean>
<service ref="featuresPlugin" interface="javax.servlet.Servlet" >
diff --git a/karaf/webconsole/src/main/resources/res/ui/features.js b/karaf/webconsole/src/main/resources/res/ui/features.js
new file mode 100644
index 0000000..eef0b4d
--- /dev/null
+++ b/karaf/webconsole/src/main/resources/res/ui/features.js
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function renderFeatures( data ) {
+ $(document).ready( function() {
+ renderView();
+ renderData( data );
+ $("#repository_table").tablesorter( {
+ headers: {
+ 1: { sorter: false }
+ },
+ sortList: [[0,0]],
+ } );
+ $("#feature_table").tablesorter( {
+ headers: {
+ 3: { sorter: false }
+ },
+ sortList: [[0,0]],
+ } );
+ } );
+}
+
+function renderView() {
+ renderStatusLine();
+ renderTable( "Feature Repositories", "repository_table", ["URL", "Actions"] );
+ var txt = "<form method='post'><div class='table'><table class='tablelayout'><tbody><tr>" +
+ "<input type='hidden' name='action' value='addRepository'/>" +
+ "<td><input id='url' type='text' name='url' style='width:100%'/></td>" +
+ "<td class='col_Actions'><input type='button' value='Add URL' onclick='addRepositoryUrl()'/></td>" +
+ "</tr></tbody></table></div></form><br/>";
+ $("#plugin_content").append( txt );
+ renderTable( "Features", "feature_table", ["Name", "Version", "Status", "Actions"] );
+ renderStatusLine();
+}
+
+function addRepositoryUrl() {
+ var url = document.getElementById( "url" ).value;
+ changeRepositoryState( "addRepository", url );
+}
+
+function renderStatusLine() {
+ $("#plugin_content").append( "<div class='fullwidth'><div class='statusline'/></div>" );
+}
+
+function renderTable( /* String */ title, /* String */ id, /* array of Strings */ columns ) {
+ var txt = "<div class='table'><table class='tablelayout'><tbody><tr>" +
+ "<td style='color:#6181A9;background-color:#e6eeee'>" +
+ title + "</td></tr></tbody></table>" +
+ "<table id='" + id + "' class='tablelayout'><thead><tr>";
+ for ( var name in columns ) {
+ txt = txt + "<th class='col_" + columns[name] + "' style='border-top:#e6eeee'>" + columns[name] + "</th>";
+ }
+ txt = txt + "</tr></thead><tbody></tbody></table></div>";
+ $("#plugin_content").append( txt );
+}
+
+function renderData( /* Object */ data ) {
+ renderStatusData( data.status );
+ renderRepositoryTableData( data.repositories );
+ renderFeatureTableData( data.features );
+}
+
+function renderStatusData( /* String */ status ) {
+ $(".statusline").empty().append( status );
+}
+
+function renderRepositoryTableData( /* array of Objects */ repositories ) {
+ var trElement;
+ var input;
+ $("#repository_table > tbody > tr").remove();
+ for ( var idx in repositories ) {
+ trElement = tr( null, { id: "repository-" + idx } );
+ renderRepositoryData( trElement, repositories[idx] );
+ $("#repository_table > tbody").append( trElement );
+ }
+ $("#repository_table").trigger( "update" );
+}
+
+function renderRepositoryData( /* Element */ parent, /* Object */ repository ) {
+ parent.appendChild( td( null, null, [text( repository.url )] ) );
+
+ var actionsTd = td( null, null );
+ var div = createElement( "div", null, {
+ style: { "text-align": "left"}
+ } );
+ actionsTd.appendChild( div );
+
+ for ( var a in repository.actions ) {
+ repositoryButton( div, repository.url, repository.actions[a] );
+ }
+ parent.appendChild( actionsTd );
+}
+
+function repositoryButton( /* Element */ parent, /* String */ url, /* Obj */ action ) {
+ if ( !action.enabled ) {
+ return;
+ }
+
+ var input = createElement( "input", null, {
+ type: 'image',
+ style: {"margin-left": "10px"},
+ title: action.title,
+ alt: action.title,
+ src: imgRoot + '/bundle_' + action.image + '.png'
+ } );
+ $(input).click( function() {changeRepositoryState( action.op, url )} );
+
+ if ( !action.enabled ) {
+ $(input).attr( "disabled", true );
+ }
+ parent.appendChild( input );
+}
+
+function changeRepositoryState( /* String */ action, /* String */ url ) {
+ $.post( pluginRoot, {"action": action, "url": url}, function( data ) {
+ renderData( data );
+ }, "json" );
+}
+
+function renderFeatureTableData( /* array of Objects */ features ) {
+ $("#feature_table > tbody > tr").remove();
+ for ( var idx in features ) {
+ var trElement = tr( null, { id: "feature-" + idx } );
+ renderFeatureData( trElement, features[idx] );
+ $("#feature_table > tbody").append( trElement );
+ }
+ $("#feature_table").trigger( "update" );
+}
+
+function renderFeatureData( /* Element */ parent, /* Object */ feature ) {
+ parent.appendChild( td( null, null, [ text( feature.name ) ] ) );
+ parent.appendChild( td( null, null, [ text( feature.version ) ] ) );
+ parent.appendChild( td( null, null, [ text( feature.state ) ] ) );
+ var actionsTd = td( null, null );
+ var div = createElement( "div", null, {
+ style: { "text-align": "left"}
+ } );
+ actionsTd.appendChild( div );
+
+ for ( var a in feature.actions ) {
+ featureButton( div, feature.name, feature.version, feature.actions[a] );
+ }
+ parent.appendChild( actionsTd );
+}
+
+function featureButton( /* Element */ parent, /* String */ name, /* String */ version, /* Obj */ action ) {
+ if ( !action.enabled ) {
+ return;
+ }
+
+ var input = createElement( "input", null, {
+ type: 'image',
+ style: {"margin-left": "10px"},
+ title: action.title,
+ alt: action.title,
+ src: imgRoot + '/bundle_' + action.image + '.png'
+ } );
+ $(input).click( function() {changeFeatureState( action.op, name, version )} );
+
+ if ( !action.enabled ) {
+ $(input).attr( "disabled", true );
+ }
+ parent.appendChild( input );
+}
+
+function changeFeatureState( /* String */ action, /* String */ feature, /* String */ version ) {
+ $.post( pluginRoot, {"action": action, "feature": feature, "version": version}, function( data ) {
+ renderData( data );
+ }, "json" );
+}