diff --git a/src/slurmcostmanager.js b/src/slurmcostmanager.js index 065a32c..20380c2 100644 --- a/src/slurmcostmanager.js +++ b/src/slurmcostmanager.js @@ -591,23 +591,34 @@ function UserDetails({ users }) { ); } -function Details({ details, daily }) { +function Details({ details, daily, partitions = [], accounts = [], users = [] }) { const [expanded, setExpanded] = useState(null); const [dateRange, setDateRange] = useState('30'); const [filters, setFilters] = useState({ partition: '', account: '', - department: '', - pi: '' + user: '' }); function toggle(account) { setExpanded(prev => (prev === account ? null : account)); } + const filteredDetails = details + .map(d => { + if (filters.account && d.account !== filters.account) return null; + let userList = d.users || []; + if (filters.user) { + userList = userList.filter(u => u.user === filters.user); + if (!userList.length) return null; + } + return { ...d, users: userList }; + }) + .filter(Boolean); + function exportCSV() { const rows = [['Account', 'Core Hours', 'Cost']]; - details.forEach(d => { + filteredDetails.forEach(d => { rows.push([d.account, d.core_hours, d.cost]); (d.users || []).forEach(u => { rows.push([` ${u.user}`, u.core_hours, u.cost]); @@ -648,17 +659,21 @@ function Details({ details, daily }) { React.createElement('option', { value: 'q' }, 'Q-to-date'), React.createElement('option', { value: 'y' }, 'Year') ), - ['Partition', 'Account', 'Department', 'PI'].map(name => - React.createElement( + ['Partition', 'Account', 'User'].map(name => { + const opts = + name === 'Partition' ? partitions : name === 'Account' ? accounts : users; + const key = name.toLowerCase(); + return React.createElement( 'select', { key: name, - onChange: e => - setFilters({ ...filters, [name.toLowerCase()]: e.target.value }) + value: filters[key], + onChange: e => setFilters({ ...filters, [key]: e.target.value }) }, - React.createElement('option', { value: '' }, name) - ) - ), + React.createElement('option', { value: '' }, name), + opts.map(o => React.createElement('option', { key: o, value: o }, o)) + ); + }), React.createElement('button', { onClick: exportCSV }, 'Export') ), React.createElement( @@ -681,7 +696,7 @@ function Details({ details, daily }) { React.createElement( 'tbody', null, - details.reduce((acc, d) => { + filteredDetails.reduce((acc, d) => { acc.push( React.createElement( 'tr', @@ -980,7 +995,13 @@ function App() { }), data && view === 'details' && - React.createElement(Details, { details: data.details, daily: data.daily }), + React.createElement(Details, { + details: data.details, + daily: data.daily, + partitions: data.partitions, + accounts: data.accounts, + users: data.users + }), view === 'settings' && React.createElement(Rates, { onRatesUpdated: reload }) ); } diff --git a/src/slurmdb.py b/src/slurmdb.py index 7783cd2..5398768 100644 --- a/src/slurmdb.py +++ b/src/slurmdb.py @@ -242,7 +242,7 @@ def fetch_usage_records(self, start_time, end_time): cpu_col = "cpus_req" query = ( - f"SELECT j.id_job AS jobid, j.account, a.user AS user_name, j.time_start, j.time_end, " + f"SELECT j.id_job AS jobid, j.account, j.partition, a.user AS user_name, j.time_start, j.time_end, " f"j.tres_alloc, j.{cpu_col} AS cpus_alloc FROM {job_table} AS j " f"LEFT JOIN {assoc_table} AS a ON j.id_assoc = a.id_assoc " f"WHERE j.time_start >= %s AND j.time_end <= %s" @@ -261,6 +261,9 @@ def aggregate_usage(self, start_time, end_time): 'daily_gpu': {}, 'monthly_gpu': {}, 'yearly_gpu': {}, + 'partitions': set(), + 'accounts': set(), + 'users': set(), } for row in rows: start = self._to_datetime(row['time_start']) @@ -271,6 +274,7 @@ def aggregate_usage(self, start_time, end_time): year = start.strftime('%Y') account = row.get('account') or 'unknown' user = row.get('user_name') or 'unknown' + partition = row.get('partition') or 'unknown' job = str(row.get('jobid') or 'unknown') cpus = self._parse_tres(row.get('tres_alloc'), 'cpu') if not cpus: @@ -288,6 +292,9 @@ def aggregate_usage(self, start_time, end_time): totals['daily_gpu'][day] = totals['daily_gpu'].get(day, 0.0) + gpus * dur_hours totals['monthly_gpu'][month] = totals['monthly_gpu'].get(month, 0.0) + gpus * dur_hours totals['yearly_gpu'][year] = totals['yearly_gpu'].get(year, 0.0) + gpus * dur_hours + totals['partitions'].add(partition) + totals['accounts'].add(account) + totals['users'].add(user) month_entry = agg.setdefault(month, {}) acct_entry = month_entry.setdefault( @@ -472,6 +479,9 @@ def export_summary(self, start_time, end_time): for y in sorted(set(totals['yearly']) | set(totals.get('yearly_gpu', {}))) ] summary['invoices'] = self.fetch_invoices(start_time, end_time) + summary['partitions'] = sorted(totals.get('partitions', [])) + summary['accounts'] = sorted(totals.get('accounts', [])) + summary['users'] = sorted(totals.get('users', [])) return summary