Resolved FELIX-1441 /Search manifest entries of bundles/

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@924190 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
index f09b6d8..b868234 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
@@ -112,7 +112,6 @@
 
     // templates
     private final String TEMPLATE_MAIN;
-    private final String TEMPLATE_UPLOAD;
 
     /** Default constructor */
     public BundlesServlet()
@@ -121,7 +120,6 @@
 
         // load templates
         TEMPLATE_MAIN = readTemplateFile( "/templates/bundles.html" );
-        TEMPLATE_UPLOAD = readTemplateFile( "/templates/bundles_upload.html" );
     }
 
     /**
@@ -462,20 +460,13 @@
         vars.put( "drawDetails", reqInfo.bundleRequested ? Boolean.TRUE : Boolean.FALSE );
         vars.put( "currentBundle", (reqInfo.bundleRequested && reqInfo.bundle != null ? String.valueOf(reqInfo.bundle.getBundleId()) : "null"));
 
-        if ( "upload".equals(reqInfo.pathInfo) )
-        {
-            response.getWriter().print(TEMPLATE_UPLOAD);
-        }
-        else
-        {
-            final String pluginRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT );
-            final String servicesRoot = getServicesRoot ( request );
-            StringWriter w = new StringWriter();
-            writeJSON(w, reqInfo.bundle, pluginRoot, servicesRoot, request.getLocale() );
-            vars.put( "__bundles__", w.toString());
+        final String pluginRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT );
+        final String servicesRoot = getServicesRoot ( request );
+        StringWriter w = new StringWriter();
+        writeJSON(w, reqInfo.bundle, pluginRoot, servicesRoot, request.getLocale() );
+        vars.put( "__bundles__", w.toString());
 
-            response.getWriter().print(TEMPLATE_MAIN);
-        }
+        response.getWriter().print(TEMPLATE_MAIN);
     }
 
     private void renderJSON( final HttpServletResponse response, final Bundle bundle, final String pluginRoot, final String servicesRoot, final Locale locale )
@@ -617,6 +608,10 @@
         jw.value( bundle.getBundleId() );
         jw.key( "name" );
         jw.value( Util.getName( bundle, locale ) );
+        jw.key( "fragment" );
+        jw.value( isFragmentBundle(bundle) );
+        jw.key( "stateRaw" );
+        jw.value( bundle.getState() );
         jw.key( "state" );
         jw.value( toStateString( bundle ) );
         jw.key( "version" );
@@ -624,25 +619,6 @@
         jw.key( "symbolicName" );
         jw.value( Util.getHeaderValue(bundle, Constants.BUNDLE_SYMBOLICNAME) );
 
-        jw.key( "actions" );
-        jw.array();
-
-        if ( bundle.getBundleId() != 0 )
-        {
-            if ( hasStart(bundle) )
-            {
-                action( jw, hasStart( bundle ), "start", "Start", "start" );
-            }
-            else
-            {
-                action( jw, hasStop( bundle ), "stop", "Stop", "stop" );
-            }
-            action( jw, true, "refresh", "Refresh Package Imports", "refresh" );
-            action( jw, true, "update", "Update", "update" );
-            action( jw, hasUninstall( bundle ), "uninstall", "Uninstall", "delete" );
-        }
-        jw.endArray();
-
         if ( details )
         {
             bundleDetails( jw, bundle, pluginRoot, servicesRoot, locale );
@@ -683,50 +659,11 @@
         }
     }
 
-
-    private void action( JSONWriter jw, boolean enabled, String op, String opLabel, String image ) throws JSONException
-    {
-        jw.object();
-        jw.key( "enabled" ).value( enabled );
-        jw.key( "name" ).value( opLabel );
-        jw.key( "link" ).value( op );
-        jw.key( "image" ).value( image );
-        jw.endObject();
-    }
-
     private final boolean isFragmentBundle( Bundle bundle)
     {
         return getPackageAdmin().getBundleType( bundle ) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
     }
 
-    private final boolean hasStart( Bundle bundle )
-    {
-        if ( isFragmentBundle(bundle) )
-        {
-            return false;
-        }
-        return bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED;
-    }
-
-
-    private final boolean hasStop( Bundle bundle )
-    {
-        if ( isFragmentBundle(bundle) )
-        {
-            return false;
-        }
-        return bundle.getState() == Bundle.ACTIVE;
-    }
-
-
-    private static final boolean hasUninstall( Bundle bundle )
-    {
-        return bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED
-            || bundle.getState() == Bundle.ACTIVE;
-
-    }
-
-
     private final void bundleDetails( JSONWriter jw, Bundle bundle, final String pluginRoot, final String servicesRoot, final Locale locale)
         throws JSONException
     {
diff --git a/webconsole/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties b/webconsole/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
index d9d2b86..d12234d 100644
--- a/webconsole/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
+++ b/webconsole/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
@@ -128,7 +128,7 @@
 bundles.classpath=Bundle Classpath
 bundles.pkg.exported=Предотставяни пакети
 bundles.pkg.imported=Използвани пакети
-bundles.pkg.importingBundles=Използвани бъндъли
+bundles.pkg.importingBundles=Използващи бъндъли
 bundles.manifest.headers=Manifest Headers
 bundles.hosts=Host Bundles
 bundles.framents=Закачени фрагменти
@@ -140,6 +140,19 @@
 bundles.upload.caption=Качване/инсталиране на бъндъли
 bundles.upload.start=Стартиране
 bundles.upload.level=Стартиращо ниво
+# filter
+bundles.filter.apply=Прилагане филтър
+bundles.filter.clear=Изчистване филтър
+bundles.filter.help=Низ или регулярен израз, който се съдържа в ID-то, името, символното име или версията на бъндъла.
+# states
+bundles.state.1=Деинсталиран
+bundles.state.2=Инсталиран
+bundles.state.4=Ресолвнат
+bundles.state.8=Стартиран
+bundles.state.16=Спрян
+bundles.state.32=Активен
+bundles.state.unknown=Непознат статус: {0}
+bundles.state.fragment=Фрагмент
 
 
 # Components plugin
diff --git a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
index 1e2e4fd..cd33fd6 100644
--- a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -140,6 +140,19 @@
 bundles.upload.caption=Upload / Install Bundles
 bundles.upload.start=Start Bundle
 bundles.upload.level=Start Level
+# filter
+bundles.filter.apply=Apply Filter
+bundles.filter.clear=Clear Filter
+bundles.filter.help=A string or RegExp expression which is matched againse bundle id, name, symbolic name or version.
+# states
+bundles.state.1=Uninstalled
+bundles.state.2=Installed
+bundles.state.4=Resolved
+bundles.state.8=Starting
+bundles.state.16=Stopping
+bundles.state.32=Active
+bundles.state.unknown=Unknown State: {0}
+bundles.state.fragment=Fragment
 
 
 # Components plugin
diff --git a/webconsole/src/main/resources/res/ui/bundles.css b/webconsole/src/main/resources/res/ui/bundles.css
index f6adee8..086a196 100644
--- a/webconsole/src/main/resources/res/ui/bundles.css
+++ b/webconsole/src/main/resources/res/ui/bundles.css
@@ -22,3 +22,5 @@
 
 .col_Status   { width: 50px;  }
 .col_Actions { width: 121px; }
+.filterBox      { float: left; margin-left: 1em }
+#uploadDialog, div.ui-dialog-buttonpane button { font-size: 50% }
diff --git a/webconsole/src/main/resources/res/ui/bundles.js b/webconsole/src/main/resources/res/ui/bundles.js
index 2660b74..b7467d3 100644
--- a/webconsole/src/main/resources/res/ui/bundles.js
+++ b/webconsole/src/main/resources/res/ui/bundles.js
@@ -14,14 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// ui elements
+var uploadDialog = false;
+var bundlesTable    = false;
+var bundlesBody     = false;
+var bundlesTemplate = false;
 
-function renderData( eventData )  {
+function renderData( eventData, filter )  {
+	lastBundleData = eventData;
 	var s = eventData.s;
-    $(".statline").html(i18n.statline.msgFormat(s[0], s[1], s[2], s[3], s[4]));
-    $("#plugin_table > tbody > tr").remove();
+    $('.statline').html(i18n.statline.msgFormat(s[0], s[1], s[2], s[3], s[4]));
+	bundlesBody.empty();
     for ( var idx in eventData.data ) {
         if ( currentBundle == null || !drawDetails || currentBundle == eventData.data[idx].id) {
-            entry( eventData.data[idx] );
+            entry( eventData.data[idx], filter );
         }
     }
     if ( drawDetails && eventData.data.length == 1 ) {
@@ -34,94 +40,66 @@
     initStaticWidgets();
 }
 
-function entry( /* Object */ dataEntry ) {
-    var trElement = tr( null, { id: "entry" + dataEntry.id } );
-    entryInternal( trElement,  dataEntry );
-    $("#plugin_table > tbody").append(trElement);   
+function entry( /* Object */ bundle, filter ) {
+	var matches = !(filter && typeof filter.test == 'function') ? true :
+		filter.test(bundle.id) || filter.test(bundle.name) || filter.test(bundle.symbolicName) || filter.test(bundle.version);
+
+	if (matches) entryInternal( bundle ).appendTo(bundlesBody);
 }
 
-function actionButton( /* Element */ parent, /* string */ id, /* Obj */ action ) {
-    if ( !action.enabled ) {
-        return;
-    }
-    var enabled = action.enabled;
-    var op = action.link;
-    var opLabel = action.name;
-    var img = action.image;
-    // fixup JQuery UI icons
-    if(img == "start" ) img = "play";
-    if(img == "update") img = "transferthick-e-w";
-    if(img == "delete") img = "trash";
-
-	// apply i18n
-	opLabel = i18n[opLabel] ? i18n[opLabel] : opLabel;
-    
-    var input = createElement('li', 'dynhover', {
-        title: opLabel
-    });
-    $(input)
-        .html('<span class="ui-icon ui-icon-'+img+'"></span>')
-        .click(function() {changeDataEntryState(id, op)});
-
-    if (!enabled) {
-        $(input).attr("disabled", true).addClass("ui-state-disabled");
-    }
-    parent.appendChild( input );
+function hasStart(b) { return (!b.fragment) && (b.stateRaw == 2 || b.stateRaw == 4) } // !isFragment && (installed | resolved)
+function hasStop(b)  { return (!b.fragment) && (b.stateRaw == 32) } // !isFragment && active
+function hasUninstall(b)  { return b.stateRaw == 2 || b.stateRaw == 4 || b.stateRaw == 32 } // installed | resolved | active
+function stateString(b) {
+	var s = b.stateRaw;
+	return  b.fragment && s == 4 ? 
+		i18n.state.fragment : // fragment & resolved
+		i18n.state[s] ? i18n.state[s] : i18n.state.unknown.msgFormat(s)
 }
 
-function entryInternal( /* Element */ parent, /* Object */ dataEntry ) {
-    var id = dataEntry.id;
-    var name = dataEntry.name;
-    var state = dataEntry.state;
+function entryInternal( /* Object */ bundle ) {
+	var tr = bundlesTemplate.clone();
+    var id = bundle.id;
+    var name = bundle.name;
 
-    // right arrow
-    var inputElement = createElement('span', 'ui-icon ui-icon-triangle-1-e', {
-        title: "Details",
-        id: 'img' + id,
-        style: {display: "inline-block"}
-    });
-    $(inputElement).click(function() {showDetails(id)});
-    var titleElement;
-    if ( drawDetails ) {
-        titleElement = text(name);
-    } else {
-        titleElement = createElement ("a", null, {
-            href: window.location.pathname + "/" + id
-        });
-        titleElement.appendChild(text(name));
-    }
-    
-    parent.appendChild( td( null, null, [ text( id ) ] ) );
-    parent.appendChild( td( null, null, [ inputElement, text(" "), titleElement ] ) );
-    parent.appendChild( td( null, null, [ text( dataEntry.version ) ] ) );
-    parent.appendChild( td( null, null, [ text( dataEntry.symbolicName ) ] ) );
-    parent.appendChild( td( null, null, [ text( state ) ] ) );
-    var actionsTd = td( null, null );
-    var div = createElement('ul', 'icons ui-widget');
-    actionsTd.appendChild(div);
-    
-    for ( var a in dataEntry.actions ) {
-        actionButton( div, id, dataEntry.actions[a] );
-    }
-    parent.appendChild( actionsTd );
+	tr.attr('id', 'entry'+id);
+	tr.find('td:eq(0)').text(id);
+	tr.find('td:eq(1) span:eq(0)').attr('id', 'img'+id).click(function() {showDetails(id)});
+	tr.find('td:eq(1) span:eq(1)').html( drawDetails ? name : '<a href="' + pluginRoot + '/' + id + '">' + name + '</a>' );
+	tr.find('td:eq(2)').text( bundle.version );
+	tr.find('td:eq(3)').text( bundle.symbolicName );
+	tr.find('td:eq(4)').text( stateString(bundle) );
+	if (id == 0) { // system bundle has no actions
+		tr.find('td:eq(5) ul').addClass('ui-helper-hidden');
+	} else {
+		var start   = tr.find('td:eq(5) ul li:eq(0)');
+		var stop    = tr.find('td:eq(5) ul li:eq(1)');
+		var refresh = tr.find('td:eq(5) ul li:eq(2)').click(function() {changeDataEntryState(id, 'refresh')});
+		var update  = tr.find('td:eq(5) ul li:eq(3)').click(function() {changeDataEntryState(id, 'update')});
+		var remove  = tr.find('td:eq(5) ul li:eq(4)');
+		start = hasStart(bundle) ?
+			start.click(function() {changeDataEntryState(id, 'start')}) :
+			start.addClass('ui-helper-hidden');
+		stop = hasStop(bundle) ?
+			stop.click(function() {changeDataEntryState(id, 'stop')}) :
+			stop.addClass('ui-helper-hidden');
+		remove = hasUninstall(bundle) ?
+			remove.click(function() {changeDataEntryState(id, 'uninstall')}) :
+			remove.addClass('ui-helper-hidden');
+	}
+	return tr;
 }
 
 function loadData() {
-    $.get(pluginRoot + "/.json", null, function(data) {
-        renderData(data);
-    }, "json"); 
+    $.get(pluginRoot + "/.json", null, renderData, "json"); 
 }
 
 function changeDataEntryState(/* long */ id, /* String */ action) {
-    $.post(pluginRoot + "/" + id, {"action":action}, function(data) {
-        renderData(data);
-    }, "json"); 
+    $.post(pluginRoot + "/" + id, {"action":action}, renderData, "json"); 
 }
 
 function refreshPackages() {
-    $.post(window.location.pathname, {"action": "refreshPackages"}, function(data) {
-        renderData(data);
-    }, "json"); 
+    $.post(pluginRoot, {"action": "refreshPackages"}, renderData, "json"); 
 }
 
 function showDetails( id ) {
@@ -196,18 +174,47 @@
     }
 }
 
+
 $(document).ready(function(){
-	$(".refreshPackages").click(refreshPackages);
-	$(".reloadButton").click(loadData);
-	$(".installButton").click(function() {
-		document.location = pluginRoot + "/upload";
+	$('.refreshPackages').click(refreshPackages);
+	$('.reloadButton').click(loadData);
+	$('.installButton').click(function() {
+		uploadDialog.dialog('open');
+		return false;
 	});
-	renderData(__bundles__);
+
+	// filter
+	$('input.filter').click(function() {$(this).val('')});
+	$('.filterApply').click(function() {
+		var el = $(this).parent().find('input.filter');
+		var filter = el.length && el.val() ? new RegExp(el.val()) : false;
+		renderData(lastBundleData, filter);
+	});
+	$('.filterForm').submit(function() {
+		$(this).find('.filterApply').click();
+		return false;
+	});
+	$('.filterClear').click(function() {
+		$('input.filter').val('');
+		renderData(lastBundleData);
+	});
+
+	// upload dialog
+	var uploadDialogButtons = {};
+	uploadDialogButtons[i18n.install_update] = function() {
+		$(this).find('form').submit();
+	}
+	uploadDialog = $('#uploadDialog').dialog({
+		autoOpen: false,
+		modal   : true,
+		width   : '50%',
+		buttons : uploadDialogButtons
+	});
 
 	// check for cookie
 	var cv = $.cookies.get("webconsolebundlelist");
 	var lo = (cv ? cv.split(",") : [1,0]);
-	$("#plugin_table").tablesorter({
+	bundlesTable = $("#plugin_table").tablesorter({
 		headers: {
 			0: { sorter:"digit" },
 			5: { sorter: false }
@@ -215,8 +222,12 @@
 		textExtraction:mixedLinksExtraction,
 		sortList: cv ? [lo] : false
 	}).bind("sortEnd", function() {
-		var table = $("#plugin_table").eq(0).attr("config");
-		$.cookies.set("webconsolebundlelist", table.sortList.toString());
+		bundlesTable.eq(0).attr("config");
+		$.cookies.set("webconsolebundlelist", bundlesTable.sortList.toString());
 	});
+	bundlesBody     = bundlesTable.find('tbody');
+	bundlesTemplate = bundlesBody.find('tr').clone();
+
+	renderData(lastBundleData);
 });
 
diff --git a/webconsole/src/main/resources/templates/bundles.html b/webconsole/src/main/resources/templates/bundles.html
index 4221f30..a62da40 100644
--- a/webconsole/src/main/resources/templates/bundles.html
+++ b/webconsole/src/main/resources/templates/bundles.html
@@ -1,10 +1,11 @@
-<script type="text/javascript" src="${appRoot}/res/ui/bundles.js"></script>
+<script type="text/javascript" src="${appRoot}/res/lib/jquery.multifile-1.4.6.js"></script>
+<script type="text/javascript" src="${appRoot}/res/ui/bundles.js"></script>
 <script type="text/javascript">
 // <![CDATA[
 var startLevel = ${startLevel};
 var drawDetails = ${drawDetails};
 var currentBundle = ${currentBundle};
-var __bundles__ = ${__bundles__};
+var lastBundleData = ${__bundles__};
 var i18n = {
 	'Symbolic Name'       : '${bundles.name.symb}',
 	'Version'             : '${version}',
@@ -30,7 +31,18 @@
 	'Uninstall'           : '${bundles.uninstall}',
 	'Refresh Package Imports' : '${bundles.refreshImports}',
 	//
-	statline              : '${bundles.statline}'
+	statline              : '${bundles.statline}',
+	install_update        : '${bundles.install_or_update}',
+	state                 : {
+		1  : '${bundles.state.1}', // uninstalled
+		2  : '${bundles.state.2}', // installed
+		4  : '${bundles.state.4}', // resolved
+		8  : '${bundles.state.8}', // starting
+		16 : '${bundles.state.16}', // stopping
+		32 : '${bundles.state.32}', // active
+		'unknown' : '${bundles.state.unknown}', // Unknown State: {0}
+		'fragment' : '${bundles.state.fragment}' // Fragment
+	}
 }
 // ]]>
 </script>
@@ -39,16 +51,16 @@
 <p class="statline">&nbsp;</p>
 
 <!-- top header -->
-<form method="post" enctype="multipart/form-data" action="">
+<form method="post" enctype="multipart/form-data" action="" class="filterForm">
 	<div class="ui-widget-header ui-corner-top buttonGroup">
-		<input name="action" value="install" type="hidden" />
-		<input name="bundlestart" value="start" type="hidden" />
-		<input name="bundlestartlevel" value="5" type="hidden" />
-		<input name="bundlefile" style="margin-left: 10px;" type="file" />
-		<input value="${bundles.install_or_update}" style="margin-left: 10px;" type="submit" />
-		<button class="reloadButton" type="button" name="reload" style="margin-left: 60px;">${reload}</button>
-		<button class="installButton" type="button" name="install">${bundles.install_update}</button>
-		<button class="refreshPackages" type="button" name="refresh">${bundles.refreshPkg}</button>
+		<div class="filterBox">
+			<input class="filter" value="^Apache" title="${bundles.filter.help}" />
+			<button class="filterApply" type="button">${bundles.filter.apply}</button>
+			<button class="filterClear" type="button">${bundles.filter.clear}</button>
+		</div>
+		<button class="reloadButton" type="button" style="margin-left: 60px;">${reload}</button>
+		<button class="installButton" type="button">${bundles.install_update}</button>
+		<button class="refreshPackages" type="button">${bundles.refreshPkg}</button>
 	</div>
 </form>
 
@@ -64,23 +76,65 @@
 		</tr>
 	</thead>
 	<tbody>
-		<tr><td colspan="6">&nbsp;</td></tr>
+		<tr><!-- template -->
+			<td>&nbsp;</td><!-- ID -->
+			<td>
+				<span class="ui-icon ui-icon-triangle-1-e" style="display: inline-block" title="Show Details">&nbsp;</span>
+				<span>&nbsp;</span> <!-- here goest bundle name/link -->
+			</td>
+			<td>&nbsp;</td><!-- version -->
+			<td>&nbsp;</td><!-- symbolic name -->
+			<td>&nbsp;</td><!-- status -->
+			<td><!-- actions -->
+				<ul class="icons ui-widget">
+					<li class="dynhover" title="${start}"><span class="ui-icon ui-icon-play">&nbsp;</span></li>
+					<li class="dynhover" title="${stop}"><span class="ui-icon ui-icon-stop">&nbsp;</span></li>
+					<li class="dynhover" title="${bundles.refreshImports}"><span class="ui-icon ui-icon-refresh">&nbsp;</span></li>
+					<li class="dynhover" title="${bundles.update}"><span class="ui-icon ui-icon-transferthick-e-w">&nbsp;</span></li>
+					<li class="dynhover" title="${bundles.uninstall}"><span class="ui-icon ui-icon-trash">&nbsp;</span></li>
+				</ul>
+			</td>
+		</tr>
 	</tbody>
 </table>
 
 <!-- bottom header -->
-<form method="post" enctype="multipart/form-data" action="">
+<form method="post" enctype="multipart/form-data" action="" class="filterForm">
 	<div class="ui-widget-header ui-corner-bottom buttonGroup">
-		<input name="action" value="install" type="hidden" />
-		<input name="bundlestart" value="start" type="hidden" />
-		<input name="bundlestartlevel" value="5" type="hidden" />
-		<input name="bundlefile" style="margin-left: 10px;" type="file" />
-		<input value="${bundles.install_or_update}" style="margin-left: 10px;" type="submit" />
-		<button class="reloadButton" type="button" name="reload" style="margin-left: 60px;">${reload}</button>
-		<button class="installButton" type="button" name="install">${bundles.install_update}</button>
-		<button class="refreshPackages" type="button" name="refresh">${bundles.refreshPkg}</button>
+		<div class="filterBox">
+			<input class="filter" value="^Apache" title="${bundles.filter.help}" />
+			<button class="filterApply" type="button">${bundles.filter.apply}</button>
+			<button class="filterClear" type="button">${bundles.filter.clear}</button>
+		</div>
+		<button class="reloadButton" type="button" style="margin-left: 60px;">${reload}</button>
+		<button class="installButton" type="button">${bundles.install_update}</button>
+		<button class="refreshPackages" type="button">${bundles.refreshPkg}</button>
 	</div>
 </form>
 
 <!-- status line -->
 <p class="statline">&nbsp;</p>
+
+<div id="uploadDialog" class="ui-helper-hidden" title="${bundles.upload.caption}">
+	<form method="post" enctype="multipart/form-data" action="${pluginRoot}">
+	<table class="nicetable">
+		<tr>
+			<td style="text-align:right">${bundles.upload.start}</td>
+			<td>
+				<input type="hidden" name="action" value="install"/>
+				<input type="checkbox" name="bundlestart" value="start"/>
+			</td>
+		</tr>
+		<tr>
+			<td style="text-align:right">${bundles.upload.level}</td>
+			<td><input type="text" name="bundlestartlevel" id="bundlestartlevel" value="${startLevel}" size="4"/></td>
+		</tr>
+		<tr>
+			<td>&nbsp;</td>
+			<td>
+				<input class="multi" accept="jar" type="file" name="bundlefile" />
+			</td>
+		</tr>
+	</table>
+	</form>
+</div>