/* * This file is part of Cockpit. * * Copyright (C) 2016 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ "use strict"; var React = require('react'); require('./listing.less'); /* entry for an alert in the listing, can be expanded (with details) or standard * rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the * columns list of columns to show in the header * columns to show, can be a string, react component or object with { name: 'name', 'header': false } * 'header' (or if simple string) defaults to false * in case 'header' is true, is used for the entries, otherwise * tabRenderers optional: list of tab renderers for inline expansion, array of objects with * - name tab name (has to be unique in the entry, used as react key) * - renderer react component * - data render data passed to the tab renderer * - presence 'always', 'onlyActive', 'loadOnDemand', default: 'loadOnDemand' * - 'always' once a row is expanded, this tab is always rendered, but invisible if not active * - 'onlyActive' the tab is only rendered when active * - 'loadOnDemand' the tab is first rendered when it becomes active, then follows 'always' behavior * if tabRenderers isn't set, item can't be expanded inline * navigateToItem optional: callback triggered when a row is clicked, pattern suggests navigation * to view expanded item details, if not set, navigation isn't available * listingDetail optional: text rendered next to action buttons, similar style to the tab headers * listingActions optional: buttons that are presented as actions for the expanded item * selectChanged optional: callback will be used when the "selected" state changes * selected optional: true if the item is selected, can't be true if row has navigation or expansion */ var ListingRow = React.createClass({ propTypes: { rowId: React.PropTypes.string, columns: React.PropTypes.array.isRequired, tabRenderers: React.PropTypes.array, navigateToItem: React.PropTypes.func, listingDetail: React.PropTypes.node, listingActions: React.PropTypes.arrayOf(React.PropTypes.node), selectChanged: React.PropTypes.func, selected: React.PropTypes.bool }, getDefaultProps: function () { return { tabRenderers: [], navigateToItem: null, }; }, getInitialState: function() { return { expanded: false, // show expanded view if true, otherwise one line compact activeTab: 0, // currently active tab in expanded mode, defaults to first tab loadedTabs: {}, // which tabs were already loaded - this is important for 'loadOnDemand' setting // contains tab indices selected: this.props.selected, // whether the current row is selected }; }, handleNavigateClick: function(e) { // only consider primary mouse button if (!e || e.button !== 0) return; this.props.navigateToItem(); }, handleExpandClick: function(e) { // only consider primary mouse button if (!e || e.button !== 0) return; var willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0; this.setState( { expanded: willBeExpanded }); var loadedTabs = {}; // unload all tabs if not expanded if (willBeExpanded) { // see if we should preload some tabs var tabIdx; var tabPresence; for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { if ('presence' in this.props.tabRenderers[tabIdx]) tabPresence = this.props.tabRenderers[tabIdx].presence; else tabPresence = 'default'; // the active tab is covered by separate logic if (tabPresence == 'always') loadedTabs[tabIdx] = true; } // ensure the active tab is loaded loadedTabs[this.state.activeTab] = true; } this.setState( { loadedTabs: loadedTabs }); e.stopPropagation(); e.preventDefault(); }, handleSelectClick: function(e) { // only consider primary mouse button if (!e || e.button !== 0) return; var selected = !this.state.selected; this.setState( { selected: selected }); if (this.props.selectChanged) this.props.selectChanged(selected); e.stopPropagation(); e.preventDefault(); }, handleTabClick: function(tabIdx, e) { // only consider primary mouse button if (!e || e.button !== 0) return; var prevTab = this.state.activeTab; var prevTabPresence = 'default'; var loadedTabs = this.state.loadedTabs; if (prevTab !== tabIdx) { // see if we need to unload the previous tab if ('presence' in this.props.tabRenderers[prevTab]) prevTabPresence = this.props.tabRenderers[prevTab].presence; if (prevTabPresence == 'onlyActive') delete loadedTabs[prevTab]; // ensure the new tab is loaded and update state loadedTabs[tabIdx] = true; this.setState({ loadedTabs: loadedTabs, activeTab: tabIdx }); } e.stopPropagation(); e.preventDefault(); }, render: function() { var self = this; // only enable navigation if a function is provided and the row isn't expanded (prevent accidental navigation) var allowNavigate = !!this.props.navigateToItem && !this.state.expanded; var headerEntries = this.props.columns.map(function(itm) { if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm)) return ({itm}); else if ('header' in itm && itm.header) return ({itm.name}); else if ('tight' in itm && itm.tight) return ({itm.name || itm.element}); else return ({itm.name}); }); var allowExpand = (this.props.tabRenderers.length > 0); var expandToggle; if (allowExpand) { expandToggle = ; } else { expandToggle = ; } var listingItemClasses = ["listing-ct-item"]; if (!allowNavigate) listingItemClasses.push("listing-ct-nonavigate"); if (!allowExpand) listingItemClasses.push("listing-ct-noexpand"); var allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined); var clickHandler; if (allowSelect) { clickHandler = this.handleSelectClick; if (this.state.selected) listingItemClasses.push("listing-ct-selected"); } else { if (allowNavigate) clickHandler = this.handleNavigateClick; else clickHandler = this.handleExpandClick; } var listingItem = ( {expandToggle} {headerEntries} ); if (this.state.expanded) { var links = this.props.tabRenderers.map(function(itm, idx) { return (
  • {itm.name}
  • ); }); var tabs = []; var tabIdx; var Renderer; var rendererData; var row; for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { Renderer = this.props.tabRenderers[tabIdx].renderer; rendererData = this.props.tabRenderers[tabIdx].data; if (tabIdx !== this.state.activeTab && !(tabIdx in this.state.loadedTabs)) continue; row =