Skip to content
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
47 changes: 34 additions & 13 deletions src/slurmcostmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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(
Expand All @@ -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',
Expand Down Expand Up @@ -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 })
);
}
Expand Down
12 changes: 11 additions & 1 deletion src/slurmdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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'])
Expand All @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -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


Expand Down
Loading